Unity-5-x-2D-游戏开发入门指南-全-
Unity 5.x 2D 游戏开发入门指南(全)
原文:
zh.annas-archive.org/md5/5f50ae4ce65cfcb489085cc63f62aac6译者:飞龙
前言
当 Packt 出版社邀请我写这本书时,我们心中的想法比你现在手中的这本书要谦虚得多。在我写作的过程中,我意识到书架上可以找到许多只教授基础知识而不提供如何将知识用于实际制作游戏的实用见解的书。因此,我稍微偏离了这本书的路线。结果,它变成了一本关于 Unity 的相当扎实的指南,特别关注 2D 游戏开发。你手中拿着的这本书(无论是纸质版还是*板电脑上的电子版)是经过大量努力创造出来的,旨在为读者提供易于理解的内容,同时不牺牲内容的完整性或不同工具的实用性。因此,所有内容都进行了详细解释,并附有许多使用示例。
此外,我觉得许多书的另一个重大缺陷是缺乏作业部分,这些部分为读者提供了练习来提高他们的技能。实际上,我相信这些部分很重要,因为它们提供了一个共同的基础,以便你在挑战自己、与朋友、课程伙伴或同事讨论时,可以将讨论超越书本。在游戏开发领域,合作很重要,因为游戏往往是许多有才华和热情的人协调努力的结果。
对于这本书,我决定通过构建一个以蛋糕、糖霜和甜食熊猫(它们会试图咬掉美味玩家的蛋糕)为主题的塔防游戏,来支持我们将会遇到的不同工具的学习。这个项目将引导读者贯穿整本书,并帮助他们发展实用的技能集。
无论你是渴望知识的学生,还是寻找教材的教授,或者是尝试 Unity(或愿意扩展你对 Unity 的理解)的游戏开发专家,或者只是一个充满热情的业余爱好者,我都希望你会喜欢这本书。
本书涵盖内容
第一章, 《*面世界中的 Unity》是 Unity 2D 世界的入门介绍。你将了解如何设置你的项目、导入资源,并使它们准备好使用。特别是,我们将详细讲解如何使用精灵编辑器。
第二章, 《烘焙蛋糕塔》教你如何在游戏中集成代码。我们将涵盖 Unity 的重要和基本概念,并展示如何通过创建我们正在构建的塔防游戏中蛋糕塔的行为来编写游戏对象脚本。
第三章, 与玩家沟通 – 用户界面,处理了通过使用用户界面(UI)向玩家提供反馈的重要任务。你将通过发现它们背后的基本原则来学习如何设计它们,并学习如何在 Unity 的 UI 系统中实现任何 UI。
第四章, 不再孤单 – 甜食爱好者熊猫出击,向我们介绍了可怕的甜食爱好者熊猫,它们将试图偷走玩家的蛋糕。你将学习如何通过使用 Unity Mechanim强大的动画系统,从 Sprite-sheets 开始,使角色栩栩如生。
第五章, 秘密成分是一点点物理,带你深入物理学的秘密,并以清晰易懂的方式解释它们。你将掌握物理学的基础知识,并学习如何使用 Unity 的 2D 物理引擎。
第六章, 漫过糖果之海 – 人工智能导航,是介绍应用于视频游戏的人工智能宇宙的入门。你将学习基本原理以及如何在 Unity 中实现 2D(甚至 3D)游戏的导航系统,以便我们可以让我们的糟糕甜食爱好者熊猫移动!
第七章, 交易纸杯蛋糕和蛋糕的终极之战 – 游戏玩法编程,总结了前几章中我们看到和学到的所有内容,以结束塔防游戏。特别是,我们将处理游戏玩法编程,这是你游戏不同部分之间的粘合剂。
第八章, 蛋糕之外是什么?,探讨了我们的游戏以及一般游戏开发的不同方面。你将发现不同的技巧、窍门和建议,如何改进我们在本书中构建的游戏,以及提高你自己的技能,成为一名更好的游戏开发者。然后,章节将为你解释游戏开发流程的部分,这些部分并不严格与游戏本身的发展相关。因此,你将深入不同的话题,从测试、优化、团队管理和工作、文档、准备发布你的游戏、市场营销、社交媒体、游戏保护,甚至本地化。
你需要这本书的
要通读本书涵盖的主题,你需要两样重要的事情:
-
任何版本的 Unity,可能是 5.x,因为这本书是为这些版本编写的。正如你所知道的那样,第一章,“Unity 中的*面世界”,将会重复,你可以从 Unity 官方网站获取:
www.unity3d.com。 -
你们的热情和激情,为了共同在游戏开发的世界中踏上一次精彩的旅程!
本书面向对象
包括学生、游戏爱好者以及想要将本书作为其课程和班级一部分的学术教授在内的任何人。
惯例
在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号如下所示:“所有这些都被放置在一个名为 Assets 的文件夹中,该文件夹位于 Project 文件夹内。”
代码块以如下方式设置:
public AwesomeUnityDeveloper GettingStarted(Yourself you) {
you.ReadThisBook();
return you;
}
当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:
public AwesomeUnityDeveloper GettingStarted(Yourself you) {
you.ReadThisBook();
return you;
}
新术语和重要单词以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“首先,如果你需要在不同 2D 和 3D 模式之间切换,你可以通过导航到编辑|项目设置|编辑器”来实现。”
注意
警告或重要注意事项以如下框的形式出现。
提示
技巧和窍门如下所示。
读者反馈
我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者的反馈对我们来说很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。要发送一般反馈,请简单地发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及书籍的标题。如果你在某个主题领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲拥有者,我们有许多事情可以帮助你从购买中获得最大收益。
下载示例代码
你可以从你的账户在 www.packtpub.com 下载这本书的示例代码文件。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给你。
你可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持标签上。
-
点击代码下载与勘误表。
-
在搜索框中输入书籍名称。
-
选择您想要下载代码文件的书籍。
-
从下拉菜单中选择您购买此书的来源。
-
点击代码下载。
文件下载完成后,请确保使用最新版本的软件解压或提取文件夹:
-
适用于 Windows 的 WinRAR / 7-Zip
-
适用于 Mac 的 Zipeg / iZip / UnRarX
-
适用于 Linux 的 7-Zip / PeaZip
本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Getting-Started-with-Unity-5.x-2D-Game-Development。我们还有其他来自我们丰富图书和视频目录的代码包可供在github.com/PacktPublishing/找到。查看它们吧!
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/GettingStartedwithUnity5x2DGameDevelopment_ColorImages.pdf下载此文件。
勘误
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书籍中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上版权材料的盗版是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即向我们提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过版权@packtpub.com 与我们联系,并提供疑似盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您对本书的任何方面有问题,您可以发送邮件至 questions@packtpub.com 联系我们,我们将尽力解决问题。
第一章. Unity 中的*面世界
这是我们进入 Unity 2D 游戏开发世界的旅程的开始。本书的结构旨在引导你从头开始创建一个完整的 2D 游戏,特别是塔防游戏。
尽管我们将专注于 2D 游戏开发,但我们的最终目标是学习如何使用 Unity,因此这也意味着我们将简要了解 Unity 如何处理 3D。所以,如果你以后想花时间做 3D 游戏,到这本书结束时,你将具备这样做所需的背景知识。事实上,本书的结构旨在包含每个主题尽可能多的细节,这包括历史概述和进一步阅读的参考。
在每一章中,我们都会面临不同的挑战,这将提高我们的技能。此外,本书不仅仅告诉你需要做什么(像许多其他书籍一样),它还解释了我们将遇到的不同工具以及如何使用它们。这样,我们将学会如何在不同的环境中使用和应用它们。因此,你可以将这本书作为参考手册,以加快你的工作流程。为了帮助你,我建议你使用索引,以便快速定位我们将面临的具体主题。
每章结束时,都有一个作业部分,其中包含一些与本章所讨论的主题相关的练习。当然,你可以自由地跳过这一部分,但我建议如果你觉得需要进一步提高技能,就做这些练习。
现在,本章是 Unity 中 2D 世界的介绍,以及为了创建我们的游戏需要做什么。特别是,我们将探讨以下主题:
-
2D 游戏是什么?
-
设计和开发 2D 游戏意味着什么?
-
哪里可以获取 Unity 及其不同版本
-
从外部来源下载图形包
-
如何在 Unity 中组织项目
-
在 2D 中理解 Unity
-
Sprite 是什么?
-
Sprite 渲染器组件
-
Sprite 的导入设置
-
如何使用具有所有不同模式的 Sprite 编辑器
-
为我们的游戏准备资源
-
设置场景和比例
最后一点。有时我会提到玩家和角色,以便举例或解释概念。因此,有时我会将他们称为男性,有时会称为女性(有时两者都是)。这种做法源于我的个人观点,以便不歧视两种性别。
说了这么多,让我们开始吧!
学习游戏开发
游戏开发和设计是围绕的一些最广泛的艺术作品。这是由于将游戏带入生命所需的巨大专业知识。你可以通过查看任何游戏的信用列表来了解这一点。它们非常广泛,包含了许多人名字,他们在各种角色上为游戏投入了大量时间。
就像生活中的大多数事情一样,游戏开发不仅可以通过实践学习,还可以通过迭代学习。即使你掌握了游戏开发中的许多分支之一,仍然有新东西可以学习。
无论你对 Unity 的了解程度如何,我强烈建议你遵循这本书中的每个步骤,即使你认为你已经掌握了这个主题。你永远不知道,总有新东西可以学习!
塔防游戏
塔防游戏有多种不同的风格。例如,在以下 Defense Grid: The Awakening 和 Unstoppable Gorg 的截图上,两者都是俯视等距风格的游戏。然而,它们设定在不同的世界中,有不同的目标。所以,鉴于这一点,什么是塔防游戏呢?首先,它们围绕着保护某个东西的想法,无论是建筑、资源、武器等等。这是定义该流派并推动游戏玩法的主要机制。其次,大多数塔防游戏需要经济和资源管理。例如,在每一波敌人之后,你可能会获得一定数量的虚拟货币,然后你必须将其分配给购买新的防御(武器、塔楼等)或升级。每种都有其优点,这取决于一系列因素,如你防御的弱点以及预计下一波将出现的敌人数量和强度。每一波敌人的数量和难度都会增加,因此挑战玩家战略性地管理和构建防御。目标是积累足够的资源来升级你的防御,并比下一波敌人存活更久。有时,玩家必须阻止敌人(或对手)摧毁他们的基地。
在其他情况下,玩家必须阻止敌人到达终点,因为每个成功通过的敌人都会对玩家的生命条造成伤害。

(顶部)Defense Grid: The Awakening 和 (底部)Unstoppable Gorg
在互联网上有许多地方可以找到塔防游戏。例如,Kongregate (www.kongregate.com/) 和 Newgrounds (www.newgrounds.com/) 是提供各种免费塔防游戏的网站(例如 Kingdom Rush 或 Bloons Tower Defense 5)。然而,许多游戏适用于 iOS (App Store) 和 Android (Play Store),操作系统如 Linux、OSX 和 PC(例如 Steam),以及游戏机(Playstation、Xbox)等等。
设计我们的游戏
在您甚至考虑打开电脑之前,您需要设计您的游戏。仅仅有一个大致的想法是不够的。您需要事先写下所有想法才能开始工作。游戏设计的第一个阶段是头脑风暴。不幸的是,我们在这个小节中没有时间描述不同的头脑风暴技术和方法。然而,最终的结果应该是堆满纸张,上面写满了成千上万的想法。这并不是要成为一件艺术品,而是您游戏将建立的基础。
注意
关于头脑风暴的一些信息也可以在《Unity 游戏化》这本书中找到,作者是 Packt publishing。您可以在以下链接找到它:www.packtpub.com/game-development/gamification-unity-5x。
下一步是细化您的想法,丢弃(或保留用于其他项目)您不需要的,并以一种连贯的形式组织它们。
最终结果应该类似于以下内容。
《熊猫入侵》是一款 2D 塔防游戏。实际上,饥饿的熊猫正在入侵以偷走玩家所有的糖。玩家必须使用纸杯塔来击退熊猫。玩家可以在地图上放置不同类型的纸杯塔。在每一级中,熊猫将遵循一条路径。此外,它们将在这条路径的开始处生成。最后,有一个玩家必须防守的野心勃勃的糖城堡。如果熊猫偷走太多糖,将糖量计降到零,玩家将失败他的重要任务。相反,如果玩家能够击退所有熊猫,玩家将获得胜利。然而,纸杯塔不是免费的。实际上,玩家必须使用糖果来购买它们。每次击退熊猫,玩家都将获得一定数量的糖果。此外,玩家可以使用糖果升级纸杯塔,使它们变得更强大!
从这段摘录中,你现在应该能够理解我们将在本书中做什么。你也有了一个如何写下你想法的基本概念。我强烈建议你始终进行这一步,即使你只是唯一的开发者,尤其是在你有团队的时候。
准备中
现在我们有了我们的想法,下一步就是获取 Unity。它有不同的版本:个人版(免费)、专业版、企业版。后三个版本比个人版包含更多功能。然而,这本书中涵盖的所有主题都可以使用免费版本完成。无论如何,您可以在官方网站上获取或购买 Unity:www.unity3d.com。
这是 Unity Technologies 网站上不同版本 Unity 的比较屏幕(如果您向下滚动,您将找到哪个版本包含哪些功能):

注意
这是对 Unity *年来模型定价变化的非常简短的总结。实际上,成为一名开发者也意味着要了解你周围的世界,并且拥有基本的营销知识也可能有所帮助。最初,Unity 的模型价格不允许开发者使用免费版本发布商业游戏。此外,游戏引擎没有所有功能,例如 Profiler 或 Movie Textures。拥有 Unreal Engine 的 Epic Games 公司在 2015 年 3 月通过使其游戏引擎免费(尽管它将收取游戏总收入的 5%)来改变其模型价格,也适用于商业用途(尽管它将收取游戏总收入的 5%)。一段时间后,Unity Technologies 也允许开发者使用免费版本发布商业游戏,但仍然带有水印。从 Unity 5.x 版本开始,之前仅在 Pro 版本中存在的功能也出现在免费版本中。在 2016 年初,Unity 通常有两种不同的版本:免费(或个人)和专业。后者比个人版包含更多功能,以下是两个版本的比较屏幕:

2016 年 6 月,Unity 对其之前描述的价格模式进行了更改。
一旦我们安装了 Unity,我们就可以开始创建新的项目。如果我们点击窗口右上角的新建项目按钮,Unity 会要求我们输入项目的详细信息。我们可以将其命名为Panda Invasion并选择目标路径,即项目文件将存储的位置。还有另一个需要注意的重要事项。Unity 给我们提供了选择 3D 和 2D 项目之间的可能性。这并不是一个重要的决定,因为任何时候都可以更改。然而,提前考虑游戏是 2D 还是 3D 是有用的。通过选择 2D 模式,Unity 会根据我们心中的游戏调整默认设置。我们将在下一节中看到这些设置。现在,你应该有一个看起来像以下的屏幕:

现在,我们可以按下创建项目按钮,我们就成功创建了项目,并准备好构建它。
本书假设您对 Unity 界面和 C#代码相当熟悉。如果您不熟悉,不要担心。在继续阅读本书之前,您有多种学习方式。例如,我编写了一个小型的免费电子指南,简要介绍了 Unity 的主要界面和概念。不要期望在那里学到所有需要的东西,但这是一个良好的开始。您可以在www.packtpub.com/packt/free-ebook/what-you-need-know-about-unity-5找到它。如果您是 Unity 的完全新手,请阅读这个小电子指南。完成阅读后,我还会在这里,我们可以继续我们的精彩旅程。此外,Unity 的官方手册及其文档在 Unity 游戏开发世界中是极好的伴侣。您可以在官方网站上找到它们,在docs.unity3d.com/Manual/index.html。
由于我们没有时间为我们即将开发的游戏创建自己的图形,我们需要下载一个自定义包。当然,您可以选择您最喜欢的。对于本书,我们将使用Tower Defence Pack 2包,可以从player26.com/下载。
注意
Tower Defence Pack 2提供了一系列美味的蛋糕,从臭名昭著的蓬松白色糖霜和彩色糖珠,到奢华的巧克力豆,更不用说一直以来的最爱,柠檬蛋白霜配银色果仁。它还提供了 Sugar Castle,这是糖果爱好者的家外之家!除了所有这些甜蜜的东西,还有树木、山脉、彩虹以及各种其他资产来填充您的甜蜜环境。只是要注意,其中有一些隐藏的危险,包括熊猫,所以请小心保护您的宝藏,防止被偷吃的甜食动物盗走!
该包包含我们创建塔防游戏所需的所有基本资产。它是免费的,即使是商业用途,即使需要署名。还有一个包含更多资产和一些不同精灵中的装饰以提高可定制性的高级版本。特别是,在免费版本中我们可以找到:
-
为塔防游戏设计的地图
-
恶搞熊猫(带有动画)
-
三个不同的升级等级用于蛋糕塔
-
包含每个对象的多个图标
-
以及更多资产来填充关卡!
以下图片可以给您一个关于本包包含的图形类型的概念:

因此,在进入下一节之前,请先下载此包。
记住过去,构建未来
如果您是 Unity 的新手,或者您只使用过 Unity 5.x,您可以跳过这一节,或者只是出于好奇阅读它。
在 Unity 4.x 版本(在 4.6 版本之前)和其他早期版本中,构建 2D 游戏有点困难。实际上,您需要使用一系列不同的方法和技巧来实现 2D 的错觉。实际上,所有的 2D 对象都是特定视角或特定摄像机下查看的 3D 对象,这产生了 2D 对象的错觉。
从 Unity 4.6 版本开始,尤其是自 Unity 5.x 版本以来,这不再需要。现在,Unity 内置了对 2D 游戏的支持。因此,现在有专门处理 2D 对象的组件,接下来的几节将探讨其中的一些。
组织项目
在 Unity 中组织项目有多种方式,因此提供了一定的自由度。在本节中,我们提出了一种方法,我们将在本书的项目开发中使用这种方法。
关键思想是按类型组织不同的资产(而不是像其他方法那样,按它们在关卡中的位置组织)。
首先,让我们了解 Unity 如何组织资产。所有资产都放置在一个名为Assets的文件夹中,该文件夹也位于Project文件夹内。因此,我们所有的资产都应该包含在这个文件夹或子文件夹中。为了创建一个新的文件夹,右键单击项目面板,然后选择创建 | 文件夹。结果,在您点击的文件夹内创建了一个新的文件夹。由于我们没有任何文件夹,它将是Assets文件夹的子文件夹。我们可以将其重命名为我们想要的任何名称。如果您错过了这个步骤,只需选择它并再次单击它(但不要太快,否则 Unity 会将其视为双击并打开文件夹),如下面的截图所示:

备注
需要注意的是,Unity 将忽略以下类别,以避免导入系统文件:
-
隐藏的文件夹和文件
-
以
~开头并以.结尾的文件夹和文件 -
命名为
cvs的文件夹和文件 -
扩展名为
.tmp的文件
我们需要创建以下文件夹(您只需创建加粗的文件夹,因为我们不会使用其他文件夹):
-
字体
-
图形
-
材质
-
动画(我们将在第四章中更详细地了解它们),不再孤单——甜牙熊猫出击
-
音乐和声音
-
其他资产(例如,用于存储的
.txt资产) -
物理材质
-
预制体(我们将在下一章中了解它们是什么)
-
场景
-
脚本
备注
如果您计划创建 3D 游戏,文件夹将不同,并将包含其他类型的资产,例如 3D 模型和纹理。
最后,我们应该在我们的项目面板中看到以下内容(我将在第四章中添加Animation文件夹,不再孤单——甜牙熊猫出击,当我们看到动画时,但如果您喜欢,可以立即添加):

关于您项目中的文件夹,还有其他一些事情需要了解。如果您创建具有某些特定名称的文件夹,Unity 将以特殊方式处理它们。我们不会使用它们;然而,快速查看它们是值得的:
-
编辑器(或其子文件夹):这包含编辑器脚本而不是运行时脚本。这些脚本旨在在您的游戏开发期间在 Unity 中实现新功能,并且不会包含在发布的游戏中。因此,您不能在您的场景中使用此文件夹中的任何脚本。此外,您的项目中可以包含多个编辑器文件夹(即使这会影响执行顺序)。 -
编辑器默认资源:这包含可以通过使用EditorGUIUtility.Load()函数按需由编辑器脚本加载的资源。 -
资源(或其子文件夹):这包含所有可以通过使用Resources.Load()函数按需从脚本中加载的资产。实际上,您可能需要加载场景中尚未存在的资产。与编辑器文件夹一样,您可以在项目中拥有尽可能多的这些文件夹。 -
插件:这包含用 C/C++编写的本地 DLL,可以访问第三方库、系统调用和其他 Unity 不直接提供的功能。正如其名所示,它用于实现或导入插件。 -
StreamingAssets:这包含不会包含在您的游戏主文件中,但可以从脚本中流式传输的资产。 -
WebPlayerTemplates:这包含用于目标*台为WebPlayer时的自定义宿主页面。此文件夹中的脚本将不会编译。
回到我们的文件夹,我们需要导入我们下载的包。这可以通过多种不同的方式完成,但最简单的方式是将包的文件夹拖放到我们的Graphics文件夹中。
如果您需要选择要使用的资产,在项目面板的左下角有一个滑块,允许您增加项目面板中图标的大小。当有大量资产且我们需要在不了解名称的情况下找到正确的资产,或者探索我们还不了解的新包时,此功能非常有用。以下截图中的滑块已突出显示,以方便您查看:

二维世界
当我们的项目设置为二维模式时,有几个需要注意的事项,我们将在本节中探讨。
首先,如果您需要在不同二维和三维模式之间切换,您可以通过导航到编辑 | 项目设置 | 编辑器来完成。如果您进入默认行为模式设置,您可以更改模式,如下面的截图所示:

回到我们的主界面,让我们看看 2D 模式和 3D 模式之间的主要区别。默认情况下,场景视图设置为2D,如以下截图所示:

这允许我们将场景视图锁定在xy*面上。
注意
z轴用于确定哪个对象应该首先渲染。这决定了哪些对象在前景,哪些在背景。
然后,每次我们创建一个新的场景时,随场景提供的默认摄像机总是设置为正交模式。此外,其位置设置为(0, 0, -10),而在 3D 模式中,它设置为(0, 1, -10)。你也可以通过在层次面板中选择主摄像机并查看检查器中的属性来检查这一点,如下面的截图所示:

其他区别包括使用精灵打包器的选项或默认没有实时方向光的对象。还有光照设置的变化(你可以从窗口/光照访问它们)。特别是,天空盒在新场景中是禁用的,预计算实时全局光照、烘焙全局光照和自动构建都设置为关闭。此外,环境光源带有深灰色。
在以下截图中,你可以看到默认的光照设置:

注意
在 2D 模式下默认设置的环境光源颜色的 RGB 代码是(54, 58, 66)。
然而,最重要的区别是 Unity 导入新 2D 资产的方式,但我们将详细讨论这一点在接下来的章节中。
精灵
在 Unity 中,2D 游戏的基石是精灵。你可以把它们想象成图片,但实际上,正如我们即将看到的,它们是更多的事物。实际上,一张图片可以包含多个精灵。通常,这类图片被称为精灵表。以下是我们包内一个精灵表的示例:

我们希望所有精灵都在单个图像上,而不是单独显示,原因有很多。其中最重要的原因是效率。每次你想在屏幕上渲染某些内容时,这都必须由你的电脑中的显卡来渲染。如果所有精灵都在单独的图像中,显卡将不得不处理大量的图像。结果,你的游戏会运行得较慢。
另一个使用精灵表的原因是动画。虽然 3D 动画是由描述 3D 模型如何移动的数据组成的,但 2D 动画是由帧组成的。就像电影或卡通一样,动画是由不同的图像组成的,或者在这种情况下,是精灵。每个都描述了一个瞬间,如果你足够快地改变它们,比如每秒 25 帧,你就可以产生运动的错觉。将所有帧放在一个独特的图像中既高效又井然有序。
自然地,精灵表单还有其他原因,但前两个原因应该足以让你相信精灵表单是最好的实践。另一方面,需要付出的代价是:游戏引擎需要能够在图像中区分它们。我们将在接下来的章节中看到 Unity 是如何处理这个问题的。但在我们继续之前,还有其他关于 Unity 中精灵的重要概念需要了解。
就像三维对象一样,精灵也有一个中心点。通常,这个中心点位于中间,但可以在精灵编辑器中更改。中心点是 Unity 开始所有计算的地方。例如,当你为精灵指定一个在场景中的位置时,Unity 将中心点放置在那个特定位置,然后围绕它绘制精灵。中心点对于旋转也很重要。每次我们旋转精灵时,旋转都会围绕中心点进行。换句话说,在旋转过程中,中心点是唯一不改变位置的点。
这可以通过截图更好地解释,其中箭头指示中心点的位置:

正如你所见,有一个以顺时针方向旋转 90 度的相同精灵。左边的一个中心点位于中间,而右边的一个中心点位于左侧(中心点可以通过蓝色圆圈识别)。当然,你可以通过*移使它们重合,但记住它在哪里很重要,尤其是在我们编码时,这样我们才能轻松实现我们想要的效果。
现在,关于精灵还有另一个需要考虑的方面。在二维游戏中,背景和在世界中移动的角色都被认为是精灵。然而,我们希望渲染在角色后面的背景,而不是相反。因此,精灵以一定的顺序渲染,这决定了哪个精灵应该渲染在其他精灵之上。
在 Unity 中,有两种主要方式来决定这个顺序:
-
排序层: 每个精灵渲染器,这是一个附加到游戏对象上的组件,用于渲染所选的精灵,都有一个名为排序层的变量。在那里,我们可以选择精灵将在哪个层上渲染。不同排序层的顺序可以在标签和层设置中确定(我们将在本章后面看到如何访问此菜单)。此外,排序层可以通过使用层内顺序变量为同一层内的精灵提供内部顺序,该变量始终位于精灵渲染器组件中。
-
Z 缓冲区: 由于二维对象只需要两个坐标来描述其位置(x轴和y轴),我们有一个z轴来描述深度。Unity 使用深度来确定哪个精灵应该首先渲染。由于你需要想象这是一个深度,因此只使用负值是一个好习惯。负值越大,角色或对象就越接*相机。
在计算效率方面,这些方法之间没有太大的差异。因此,两者都可以使用。实际上,它们也可以一起使用。一种通用方法是使用z轴来在视觉上组织角色。想象一个携带武器的角色。根据武器握在哪个手上以及角色面向哪个方向,武器应该渲染在角色后面还是前面。相反,排序层用于在更高层次上组织精灵,例如背景、前景、玩家、敌人等等。
然而,为了学习的目的,在这本书中,我们将不会使用排序层,而只使用 Z 缓冲区,因为它可以在代码中轻松更改。
Sprite 渲染器
在我们提到这个组件之前,可能值得多讨论一下。
每次我们向场景添加精灵时,这个组件都会自动附加。它应该看起来像以下截图:

让我们分解一下不同的参数:
-
精灵:这包含需要渲染的精灵。
-
颜色:这是一个乘以精灵图像的颜色。如果您对着色器和渲染器有些了解,这实际上是渲染网格的顶点颜色。
-
翻转:这定义了精灵需要在哪个轴上翻转。这是 Unity 5.3 中的一个新功能。
-
材质:这是 Unity 应该用来渲染精灵的材质。默认的材质对我们的需求来说已经足够了。如果您是着色器专家,有两种内置的着色器。两者都是简单的 alpha 混合着色器,但漫反射着色器与光线交互,生成一个(
0,0,-1)的前向法线向量。 -
排序层:精灵应该渲染在哪个排序层(如前所述)。
-
层中顺序:这是特定排序层内的顺序(如前所述)。
导入新朋友
如果您已经从“准备就绪”部分下载并导入了这个包,我们现在应该在我们的“项目”文件夹中拥有所有文件。如果您转到“图形/塔”文件夹并选择cupcake_tower_sheet-01,我们应该在检查器中看到以下内容:

这些是导入设置,可以设置不同的选项。在我们更改了某些内容之后,我们需要按下底部的应用按钮来确认更改。同样,如果我们不满意,我们可以按下还原按钮来放弃我们的更改。
重要的是要注意纹理类型是精灵(2D 和 UI)。在 2D 模式下,Unity 始终将图像文件导入为精灵而不是纹理。
我们需要考虑的另一个重要参数是精灵模式。默认情况下,它设置为单个,但它可以被更改为多个或多边形(仅从 Unity 5.3 开始)。正如名称所暗示的,第一个用于图像包含单个精灵时,第二个模式用于我们有一个包含多个精灵的精灵图集时。最后一个用于识别具有自定义边数的多边形精灵。
此外,每单位像素参数决定了精灵在场景中的大小。它表示在场景视图中需要多少像素来表示一个单位长度。默认情况下,它设置为100,但当你需要调整你的资产并更改它们到正确的尺寸时,你应该修改这个值。然而,如果你已经有了缩放的想法,相应地创建图形可以在开发的后期阶段节省一些时间。
关于其他设置(打包标签、生成 Mip 贴图、过滤模式、最大尺寸和格式),我们将在本书的最后一章中详细讨论,届时我们将讨论优化。
由于我们选择的文件包含多个精灵,在我们进入下一节之前,让我们将精灵模式设置为多个。
精灵编辑器
在导入设置中,还有一个名为精灵编辑器的按钮。如果我们按下这个按钮,会出现一个新的窗口。这就是精灵编辑器,正如我们可以在以下屏幕截图中看到的:

如果我们搞砸了,我们总是可以通过点击精灵编辑器右上角的还原按钮来恢复它们。旁边,你还可以找到一个应用按钮,你使用它来确认你的选择,所以请小心你按的是哪一个!
为了你的参考,它们在下一张屏幕截图中被突出显示:

在这两个按钮附*,你可以找到一些可能在你在精灵编辑器中工作时有所帮助的功能。第一个按钮很容易错过,但它允许你从彩色资产(RGB 通道)切换到 B/W(alpha 通道)。这在需要定义轮廓且图像具有透明度时特别有用,正如我们稍后将会看到的。为了避免错过它,你可以在以下屏幕截图中找到它被突出显示:

在它的右边,有两个滑块,允许你放大/缩小或增加/减少分辨率(像素数)。这些功能在以下屏幕截图中显示:

精灵编辑器允许你进行不同的操作。对于单个精灵,它提供了改变支点的可能性。对于精灵图集,例如在这个案例中,这是 Unity 理解有多少精灵以及它们在图像中位置的方式。
现在,有几种不同的方法来做这件事,所以让我们更详细地看看它们。
手动模式
在手动模式下,是你选择图像中的每个精灵,并告诉 Unity 它的位置和大小。
要创建一个新的选择,你需要点击精灵的一个角,并拖动鼠标直到你选择了整个精灵。一个绿色矩形出现,显示你选择的部分,并且你可以看到在拖动鼠标时精灵如何实时变化。如果你释放鼠标按钮,绿色矩形变成蓝色,Unity 将解释它内部的所有内容作为精灵。
你可以创建尽可能多的选择(矩形)。此外,通过点击它们,你可以将它们移动到图像的周围并改变它们的尺寸。以下是我们带有一些手动选择的精灵图集的示例:

如果你画了一个比精灵大的矩形,Unity 可以尝试裁剪它。在精灵编辑器的左上角,有裁剪按钮,如图所示:

它仅在突出显示选择时才处于活动状态。如果你不喜欢最终结果,你总是可以修改选择。
此外,在每个选择的中间有一个小蓝色圆圈。这是该选择的中心点。我们可以自由地将其拖动到另一个位置。然而,除了非常特定的情况外,将中心点放在中间是常见且有用的。所以目前,不必太担心它,只需将其留在中间即可。
你可能还会注意到,在矩形的每一边中间都有四个小绿色方块。在几个部分中,我们需要它们来进行 9 切片缩放。
一旦我们突出显示了选择,就可以通过使用出现在精灵编辑器右下角菜单中的选项来更详细地修改它。这里看起来是这样的:

从这个菜单中,你可以修改选择的名称,这将反映在我们使用精灵时的名称上。通过输入数值,你可以精确设置选择的尺寸、位置和中心点。
总结来说,手动模式在精灵图集中的精灵形状和尺寸不同时特别有用。即使图片的设计者很小心,避免将单个精灵放置得很*,物体仍然可以有非常不同的尺寸。
自动模式
在自动模式下,Unity 会尝试切割精灵,这意味着它会为你创建不同的选择。然而,为了获得更好的结果,你需要提供一些关于图像的信息。无论如何,一旦 Unity 为图像提供了选择,你仍然可以像在手动模式下一样修改它们。
在精灵编辑器的左上角,紧挨着裁剪按钮,我们可以看到切片按钮,如图所示:

点击它,会出现一个看起来像这样的菜单:

如你所见,我们可以选择不同的类型。让我们逐一了解它们。
自动类型是 Unity 对选择的最佳猜测。除了我们稍后将看到的放置选择中心点的方法外,没有其他设置。Unity 将自动完成所有操作,如果我们不需要我们的精灵具有相同的大小,这种方法效果相当不错。以下是应用于我们图像的最终结果:

自动类型包含三种不同的方法:
-
删除现有方法在切片图像之前删除所有之前的选区
-
智能方法试图为尚未选择的精灵创建选择
-
安全方法在创建新选区时不会删除之前的选区
相反,按单元格大小划分网格类型将图像划分为选区的网格。从菜单中,你可以选择每个单元格的尺寸。因此,单元格的数量将取决于它们的大小。
按单元格数量划分网格类型再次将图像划分为选区的网格。然而,这次,你可以从菜单中设置图像中的单元格数量,它们的尺寸将取决于此。以下是使用 4 x 4 网格切片的我们的图像:

多边形模式
从 Unity 5.3 开始,你可以访问精灵编辑器的新功能。为了使用它,你需要将资产的导入设置中的精灵模式设置为多边形。
在此模式下,Unity 自动将精灵作为多边形切片。一旦我们打开精灵编辑器,我们就可以立即设置多边形的边数或边。如果我们错过了这个设置,我们总是可以按下精灵编辑器左上角所示的更改形状按钮:

如果我们选择一个八边形(八边形多边形),这是我们图像中的最终结果:

UI 精灵编辑器-9 切片缩放
另一个重要的精灵编辑器功能是 9 切片。当用户界面元素需要在不同部分进行不同缩放时使用。这个功能在精灵编辑器中,因为 Unity 将用户界面元素视为精灵。
我们将在另一章中看到用户界面,但让我们先了解为什么某些用户界面元素需要以不同的方式缩放。正如你所知,游戏可以在不同屏幕上运行,这些屏幕通常具有不同的分辨率和宽高比。因此,用户界面需要根据屏幕进行适当的缩放。然而,如果你创建了一个具有美丽圆角的按钮,一旦缩放,它们的外观将与我们最初设计的完全不同,而且并不更好。
9 切片技术通过在精灵上定义九个不同缩放的区域来避免这个问题。特别是,角落将完全不缩放:只有沿着其轴的边缘和中央区域将向所有方向缩放。以下图像将帮助理解这九个区域以及它们的缩放方式:

让我们用一个 UI 图像来学习如何使用 Unity 的精灵编辑器进行 9 切片。在Graphics/UI文件夹中选择ui_blank_square_icon_pink,并在精灵编辑器中打开它。由于我们没有将其精灵模式设置为多个,所以我们只有一个围绕整个图像的选择。
正如我们已经注意到的,在我们的选择边缘有一些绿色方块。如果我们拖动它们,我们可以将图像分成九个部分,我们正在对这个精灵执行 9 切片。以下是如何使用按钮进行 9 切片的示例:

你需要将中央区域留得尽可能大,并保持其他区域适当的大小,以包括角落和边缘。
现在,我们应该对精灵编辑器有了很多了解。我建议你在进入本书的下一部分之前,先使用精灵编辑器练习一下。实际上,你需要练习我们迄今为止所介绍的方法,为我们将要构建的游戏准备所有资源。
准备资源
在本节中,你将有机会练习我们迄今为止所学到的。实际上,我们需要为我们的游戏准备资源。
让我们先选择Graphics/towers/cupcake_tower_sheet-01(我们之前使用的同一个文件)并使用 3 x 3 网格进行切片。然后,我们应该为每个精灵重命名。
在第一行,我们可以给他们这些名称(从左到右):
-
Sprinkles_Cupcake_Tower_0 -
Sprinkles_Cupcake_Tower_1 -
Sprinkles_Cupcake_Tower_2
在第二行,我们可以给他们这些名称:
-
ChocolateChip_Cupcake_Tower_0 -
ChocolateChip_Cupcake_Tower_1 -
ChocolateChip_Cupcake_Tower_2
最后,第三行:
-
Lemon_Cupcake_Tower_0 -
Lemon_Cupcake_Tower_1 -
Lemon_Cupcake_Tower_2
最后,在项目面板中,我们应该有以下内容:

使用相同的过程处理Graphics/enemies和Graphics/UI文件夹,通过分割不同的精灵图集。别忘了分配有意义的名称。在本书的其余部分,当我们提到一个资源时,其名称将不言自明。为了您的方便,我们将指定精灵被取出的原始文件。
作为关卡的场景
一个 Unity 游戏由不同的场景组成,你可以把它们想象成关卡。在项目面板中创建一个文件夹来存储所有场景是个好习惯。所以,如果我们还没有这样做,请右键点击项目面板,然后导航到创建 | 文件夹,并将其重命名为Scenes。
在工具栏菜单中,在文件下,有创建、保存和加载场景的选项。让我们保存当前的场景,即使它是空的,也可以通过导航到文件 | 保存场景,如图所示:

小贴士
您也可以使用键盘快捷键Ctrl + s (Cmd + S在 Mac OS 上)来保存场景。
Unity 会询问保存场景的位置。选择我们刚刚创建的Scenes文件夹,并将文件命名为Level_01。因此,每次我们保存场景时,Unity 都会将其保存在这个文件中。
设置比例
在构建我们的游戏时,我们需要考虑我们打算为哪个目标*台进行开发。当然,我们希望将游戏适配到尽可能多的设备和*台。因此,在游戏开发过程中,进行这种适配是一个重要的步骤。
由于这超出了本书的范围,我们不会过多地详细介绍,但重要的是要了解目标*台的屏幕比例,并据此开发游戏。对于本书,我们将坚持使用 16:9,因为它是一个常见的比例,也容易适应其他比例,而且我们下载的包是为 16:9 比例创建的。
要在 Unity 中更改比例,您需要选择游戏选项卡。在左上角,有两个下拉菜单。第一个是用于显示(从 Unity 5.3 可用)的,另一个是比例。它们在以下屏幕截图中突出显示:

注意
从 Unity 5.5 开始,在这些设置旁边还有一个滑块,允许您通过更改其比例在场景中放大或缩小。
如果您需要一个自定义比例,您可以在列表末尾选择加号按钮,然后会出现如下屏幕:

从这里,您可以为此分辨率及其维度分配一个标签。一旦添加了分辨率,它将在所有项目中共享。
在我们进入下一部分之前,请记住将项目的分辨率设置为16:9。
关于 Unity 界面的更多信息
从我在本章开头向您推荐的 e-guide 中,您应该已经对 Unity 界面有了更多的了解。但是,我想与您分享一个小技巧。
从顶部菜单栏导航并选择编辑 | 预设...,如图所示:

然后,从出现的菜单中选择第三个选项卡,颜色,在这个屏幕上您将有机会更改场景视图中主要图形元素(或 Gizmos;见下一章)的颜色,例如轴或网格,如图所示:

然而,技巧在于 Playmode 淡色 设置。它允许你在游戏运行时更改界面的整体色调。你可能现在看不到它的实用性,但当你想要修改参数时,它会非常有帮助。而且,你可能会忘记自己是在游戏模式中。我个人认为,这个小技巧在很多情况下都帮了我大忙。以下是一个示例,展示了 Unity 界面在以蓝色色调的游戏模式下的外观:

作业
在本章中,你已经花了一些时间与 Sprite 编辑器一起工作,因此没有必要再进行更多关于它的练习。然而,如果你出于某种原因想要继续练习,你可以从 Unity 资产商店(窗口 | 资产商店)下载标准资产,因为它是免费的。然后,导入 Standard Assets/2D/Sprites 文件夹,并尝试使用不同的模式从头开始切片,以实现相同的结果。
摘要
在本章中,我们已经看到了导入 Sprites 并为在游戏中使用它们做准备的全过程。在这个过程中,我们了解了 Sprites 是什么,如何导入它们,以及如何以不同的方式使用 Sprite 编辑器。此外,我们还探讨了在设置为 2D 模式下的 Unity,并了解了 2D 游戏开发的简介。最后,我们学习了如何设置场景,以适应目标*台的正确屏幕分辨率。
我认为本章我们已经涵盖了大量的内容,现在是时候休息一下了。在继续到下一章学习 2D 游戏脚本之前,去拿一杯咖啡或者甚至一块纸杯蛋糕吧。
第二章:制作纸杯蛋糕塔
在本章的第二部分,我们将开始构建我们的游戏。我们将看到如何在二维空间中放置对象并创建最常用对象的模板。此外,我们将看到 Unity 如何处理脚本,并将为我们的游戏编写几个脚本。
特别是,我们将涵盖以下主题:
-
在二维空间中放置对象
-
为我们的游戏设置地图
-
使用标签和层
-
创建预制体(游戏对象的模板)
-
创建新的脚本
-
Unity 中脚本的基本概念
-
为我们的游戏编写第一个两个脚本
与本书的所有其他章节一样,你将在末尾找到作业部分。它包含一系列不同的练习,以提高你的技能并将各种不同的功能实现到你的游戏中。
因此,让我们开始学习如何将二维对象放置到场景中。
二维对象
在上一章中,我们看到了在 Unity 中 2D 对象是精灵。然而,我们没有提到如何将它们导入场景。
将你的精灵拖放到场景中最简单的方法是从项目面板中将它们拖放到场景视图中。Unity 将自动创建一个新的游戏对象,其名称与精灵相同,并附加一个精灵渲染器。我们已经在上一章中介绍了这个组件。由于我们不会使用排序层(正如我们在上一章中决定的),在将新的精灵拖入场景时,我们不需要更改任何设置。
将精灵添加到场景的另一种方法是右键单击层次结构面板,然后选择2D 对象 | 精灵。然而,在精灵渲染器中,你需要指定要使用哪个精灵。
让我们把Pink_Sprinkle精灵拖入我们的场景(你可以在Graphics/projectiles文件夹中的projectiles_sheet_01文件中找到它)。它在场景视图中的样子如下:

由于精灵也是游戏对象,你可以访问它们的变换属性,如下所示:

这意味着你可以沿着x轴和y轴改变它们的位置,以及缩放和旋转。记住,z轴用于确定深度,正如我们在上一章中讨论的。
通过使用缩放参数,可以翻转精灵。然而,请注意,这也会翻转其子对象。正如我们在上一章中看到的,从 Unity 5.3 开始,翻转精灵时,最好使用精灵渲染器上的翻转变量。
注意
你可以通过导航到顶部菜单栏并选择游戏对象 | 创建空对象来创建一个空的游戏对象。或者,在层次结构面板上,点击创建,然后选择创建空对象。创建一个空的游戏对象在需要创建其他游戏对象的容器时非常有用,或者如果我们想从头开始构建游戏对象。
父对象
每个游戏对象都可以有一个父对象。这意味着游戏对象将随着其父对象一起移动、旋转和缩放。
我可以用很多话来解释这个概念,但有些事情视频比文字解释得更好。因此,有一个非常简短的视频解释了层次结构和父子关系:unity3d.com/learn/tutorials/topics/interface-essentials/hierarchy-and-parent-child-relationships。
我建议你在继续阅读这本书之前先看看它。我会在这里等你。
世界坐标和局部坐标之间的区别
在 Unity 中,每个游戏对象都有一个位置,但位置需要一个参考框架(关于参考框架的更多内容将在第五章,物理的秘诀是加一点调味料)。特别是,Unity 提供了两种查看(和设置)坐标的方式:
-
世界坐标:这是游戏对象所在位置的绝对坐标(通过绝对,我是指相对于世界框架,在游戏中被认为是绝对的)
-
局部坐标:这是游戏对象相对于其父对象的坐标
您可以通过 Unity 界面右上角的一个切换按钮轻松地在两种坐标之间切换,如下面的截图所示:

如前一个截图所示,它们都是切换按钮,但用于在世界坐标和局部坐标之间切换的是左侧的按钮。目前,它被选中为全局,这意味着在世界坐标上。
使用 Z-Buffering 对不同的层进行排序
在上一章中,我们决定使用 Z-Buffering 而不是排序层。然而,我们需要决定我们游戏中的哪些元素将位于前景,相对于其他元素。
此外,记住相机的设置也很重要。选择主相机,它应该是场景中唯一的相机。检查器应该看起来像这样:

如您所见,默认情况下,其位置上的Z值设置为-10。这意味着您不能在精灵的Z值中有任何更大的负值(这意味着小于-10),否则它将不会被渲染。对我们来说,-10是完美的,我们将坚持使用它。
接下来,我们需要为所有元素分配一个Z值。我们可以开始将地图放置在背景中,通过分配我们心中最低的深度(即最大的Z值);在这种情况下是零。
然后,我们希望有敌人。因此,我们可以将它们的Z值设置为-1。之后是弹丸和塔楼,分别-2和-3。最后,我们需要为前景添加另一个值。以下是总结表:
| 元素 | Z 值(深度) | 原因 |
|---|---|---|
| 地图 | 0 |
该地图具有最低的值,因为它将在所有事物之后。 |
| 敌人 | -1 |
敌人在地图之后渲染,因为它们可能会穿过塔,而我们希望保持塔的可见性。此外,在它们击中敌人之前,也应该在敌人上显示弹丸。 |
| 弹丸 | -2 |
弹丸是从塔楼发射的,所以将弹丸放在塔楼上方看起来可能有些奇怪,而从中部发射出来看起来更自然。 |
| 塔楼 | -3 |
塔楼没有其他层在其上方,除了地图叠加。 |
| 地图叠加 | -9 |
这位于前景,因此它必须最后渲染。我们选择-9而不是-4,因为我们可能还会添加其他层,但前景总是离相机最*的那一层。我们将在下一节中看到这一层包含的内容。 |
| 主相机 | -10 |
默认值。 |
在我们为游戏元素创建 Prefab 时,我们需要记住这些值。这对本章以及本书的其余部分都很重要。
注意
我们将在本章的后面讨论 Prefab 是什么。
展开地图
我们终于准备好将 2D 地图放置到我们的场景中。
在上一章中,我们已经将分辨率设置为 16:9。因此,我们将在我们的包中找到的地图已经准备好使用。
让我们从将sugar_mountain_map精灵从Graphics/maps文件夹拖入我们的场景开始。我们需要将其放置在(0,0,0)。请注意,z轴被设置为零。
这是我们需要的完美地图。例如,在左侧,有甜食爱好者将跟随的路径起点。在路径的尽头,有玩家需要保护的糖城堡。此外,顶部有足够的空间在第三章,与玩家沟通 - 用户界面中实现我们的用户界面。
下一步是修改相机设置。我们想要做的是将整个地图适应到相机视图中。为了实现这一点,只需修改大小属性为22.5,如下所示:

因此,我们的地图将在相机视图中完美居中。这就是我们在选择了相机后的场景视图中应该看到的样子:

现在,让我们将上一章中切割的其中一个纸杯蛋糕塔引入进来;例如,ChocolateChip_Cupcake_Tower_2。如果我们将其拖动到路径的起始位置,我们会遇到以下问题:

或者也在地图的底部:

事实上,纸杯蛋糕塔不应该在石头上,而应该在它的后面,这是由于透视造成的。由于我们在 2D 世界中工作,我们需要创建一个透视。幸运的是,我们的包包含我们地图的叠加层。它还包含所有应该在前景中的资产,如图所示:

注意
请注意,通常,所有资产都在不同的层级上,这样我们就可以自定义它们的位置。然而,在包中,它们已经预先放置,以便您方便使用,这样我们就不需要花费时间学习令人惊叹的事情!
因此,让我们也将这个叠加层添加到场景中。它被称为sugar_mountain_map_overlay,可以在Graphics/maps文件夹内找到。再次提醒,记得将其x和y位置设置为零。完成这些后,我们没有看到任何区别,纸杯蛋糕塔仍然悬浮在石头上。实际上,我们在上一节中已经决定,所有的z轴值都应该分配给不同的游戏元素。如果你记得,地图叠加的值是-9。
一旦你设置了地图叠加的z轴值,我们的纸杯蛋糕应该会像我们希望的那样表现:

它在这里的行为也是正确的:

地图终于准备好了。最后的润色;我们应该将地图叠加层设置为地图本身的父级。因此,如果我们需要更改地图,它们将一起移动和缩放。
在我们继续旅程之前,记得删除纸杯蛋糕塔,因为我们只需要它进行测试。
图层和标签
如果你已经知道你打算做什么,最好在开始时设置好一切。特别是,Unity 有一些标签可以分配给游戏对象。这些是图层和标签。Unity 使用这两个属性来区分某些类型的游戏对象。
默认情况下,其中一些已经被定义,但我们需要为我们的项目添加更多。从工具栏菜单中,我们可以通过导航到编辑 | 项目设置 | 标签和图层来访问图层和标签设置。因此,检查器现在应该看起来像以下截图:

注意
在这个菜单中,我们还可以更改用于渲染 2D 对象的排序图层。然而,如前所述,我们将使用 Z 缓冲区来实现相同的效果。
让我们展开标签菜单,如下所示:

要添加新标签,只需在右下角点击+按钮。我们需要添加两个标签,分别是敌人和**弹射物**,如图所示:

实际上,我们在游戏的开发过程中还需要这两个标签。实际上,当一个纸杯蛋糕塔搜索周围的对象时,它需要区分敌人和弹射物。
预制件
当场景开始充满对象时,其中一些对象可能会变得复杂。这里的复杂是指具有许多组件和子对象。如果我们需要在游戏中使用许多这样的对象,并且可能一次性更改所有这些对象,Unity 提供了创建预制件的可能性。
正如名字所暗示的,这是一个已经组装好所有必要组件并准备好放置在场景中的对象。其优点是经常可以重用它,并且可以快速更改所有实例。
注意
如果场景中的对象是一个预制件,那么在层次结构面板中它的名字是蓝色的。如果名字是红色的,那么这意味着缺少一些引用。
为了使我们的项目更有条理,让我们创建一个名为 Prefabs 的文件夹,如果我们还没有这样做的话。在文件夹内,右键单击并选择 创建/预制件。你可以按自己的意愿命名,但为了这本书,让我们保持 Pink_Sprinkle_Projectile_Prefab 这个名字。
我们已经在场景中有了喷雾效果,所以从层次结构面板中,将其拖动到 Pink_Sprinkle_Projectile_Prefab。
现在,我们可以从场景中删除之前的喷雾效果,因为我们不再需要它了。为了测试目的,你可以通过将预制件拖动到场景视图中来在场景中添加尽可能多的喷雾效果。当然,记得在继续本章的其余部分之前将它们删除。
当我们选择一个预制件的实例对象时,检查器中会显示三个额外的按钮,如下面的截图所示:

这些是它们的功能:
-
选择:这是一个快速在项目面板中选择对象预制件的快捷方式。
-
还原:如果我们对预制件实例(当前选定的对象)进行了更改,这些更改不会影响预制件。通过点击此按钮,我们将所有更改还原到原始预制件。
-
应用:另一方面,如果你对对预制件实例所做的更改感到满意,通过点击此按钮,你可以将这些更改应用到预制件上。结果,你可能会修改场景中的所有其他实例。所以当你使用这个功能时要小心。
游戏视图
你应该已经了解游戏视图,但可能有一些你之前不知道的新功能类型。因此,在我们继续旅程之前,一个简短的回顾可能是有用的。
首先,我们有三个主要按钮,你应该非常熟悉,如下所示:

第一个是播放按钮,它使你的游戏运行。第二个按钮暂停游戏,并允许你调整一些设置。最后一个按钮只让游戏运行一帧。
在左上角,我们有之前章节中提到的显示和分辨率选项卡。在对角线(右上角),有许多不同但很有用的切换按钮,如下面的截图所示:

这些是它们的功能:
-
最大化游戏播放:如果开启此选项,每次你按下播放按钮,游戏视图将最大化到最大的窗口。这对于在几乎全屏中测试游戏非常有用;否则,如果没有第二个显示器,调整值可能会有些困难。
-
静音音频:如果启用,正如其名所示,它将静音游戏中的所有音频源。
-
统计数据:如果开启此选项,它将提供一些关于游戏性能的基本反馈,如下所示:

- Gizmos:这些用于识别场景中的对象。然而,我们将在第六章通过糖果海导航——人工智能导航中看到这些,通过糖果海导航——人工智能导航。
数学背景
无论你是否喜欢数学;然而,这是一个事实,它是游戏开发所必需的。我们没有时间来探讨这个游戏背后的所有数学,因为它需要有一个完整的游戏开发工具集。然而,本节将向你介绍一些我们将需要在本书中其他部分使用的基本概念。此外,它还引用了一些官方文档,以便你可以了解更多关于它们的信息。
如果你对自己以下的一些主题感到自信,你可以自由地跳过它们:
-
向量:在游戏开发中,向量非常重要,因为它们能够描述空间(包括 3D 和 2D)。它们可以表示一个位置或一个方向。你可以在
docs.unity3d.com/Manual/VectorCookbook.html了解更多关于它们的信息,并观看这个视频:unity3d.com/learn/tutorials/topics/scripting/vector-maths。 -
概率:当我们希望从游戏中抽取样本,并包含不确定性和玩家的机会时,这一点非常重要。一个常见的例子是在即时战略(RTS)或大型多人在线角色扮演游戏(MMORPG)中,伤害量通常在一定的范围内(由角色的属性决定),但实际伤害量使用随机数。另一个例子是当攻击应该是致命的,以便造成双倍伤害。在章节末尾将解释如何在 Unity 中提取随机数。然而,考虑购买一本关于概率的适当数学书籍;这可能是值得的。
-
弧度和角度:角度可以有两种度量单位。弧度通常用于计算,但 Unity 有一些常数,通过乘法可以将一个单位转换为另一个表示。你可以在
docs.unity3d.com/ScriptReference/Mathf.Deg2Rad.html和docs.unity3d.com/ScriptReference/Mathf.Rad2Deg.html了解更多关于这些的信息。 -
三角学:这非常重要,因为正弦和余弦函数经常被用来实现可信的行为,因为大自然在我们的世界中使用了它们。不幸的是,没有捷径。因此,如果你真的想理解它们并深入其中,你应该阅读任何关于三角学的书籍。然而,最重要的概念是它们将它们的参数值范围在
-1和+1之间。 -
四元数:这是一个不太直观的数学实体,因为它涉及到复数的分析。然而,在 Unity 编程中,了解它们的细节并不重要(除非在非常具体的情况下)。实际上,知道 Unity 使用它们来存储旋转就足够了。此外,还有将欧拉表示(三个角度中最直观的)转换为其他表示的函数。这个选择背后的原因超出了本书的范围,但这是由于欧拉表示的数值不稳定性。你可以通过观看以下视频了解更多信息:
unity3d.com/learn/tutorials/topics/scripting/quaternions。 -
Atan2():这是一个在游戏开发中非常重要的函数,因为它能够计算向量的角度。你可以阅读更多关于这个函数的信息:
docs.unity3d.com/ScriptReference/Mathf.Atan2.html。
Unity 中的脚本编写
在本节中,我们将学习游戏开发中最难的话题之一!然而,我强烈鼓励你不要害怕,而是要多加练习。结果,你将能够掌握你游戏中的每一个细节。这真是太棒了!
创建新脚本
首先,我们需要了解如何在 Unity 中创建新的脚本。最简单的方法是选择一个游戏对象,在检查器中导航到添加组件 | 新脚本。这样,你仍然有机会重命名它,但脚本将位于Asset文件夹中。此外,不可能创建一个无法附加到游戏对象的类。
如果还没有这样做,一个更好的方法是创建一个名为Scripts的文件夹在项目面板中。然后,右键单击并导航到创建 | C# 脚本。结果,它将位于正确的文件夹中,如果我们创建一个无法附加到游戏对象的脚本,我们不会有问题。
在本书的其余部分,我们将假设每个新的脚本都是以这种方式创建的,并且始终位于Scripts文件夹中。
记住脚本的名称很重要,因为文件应该与脚本中的主类名相同。这意味着如果我们稍后更改类的名称,我们也需要相应地重命名文件。然而,这可能会破坏其他脚本中的某些引用,需要修正。因此,在更改名称时要小心。
要打开脚本,你需要双击它。Unity 将打开脚本编辑器。默认情况下,它将是 Monodevelop。然而,你可以通过导航到编辑 | 首选项...来更改此设置。在外部工具选项卡中,你可以更改外部脚本编辑器。另一个常用的脚本编辑器是 Visual Studio,可以从www.visualstudio.com/下载。
然而,如果你是第一次使用 Unity,我建议你坚持使用 Monodevelop。无论如何,对于这本书,我们没有任何要求(只要你能编辑脚本),所以请随意选择你最喜欢的。
脚本基础知识
如果这是你第一次在 Unity 中编写脚本,在我们开始之前,有一些信息需要了解。
注意
Unity 主要支持两种语言:C#和 JavaScript。由于我们在上一节中创建了一个 C#脚本,因此我们将在这本书的其余部分使用这种语言。
变量
变量可以是公共的、私有的或受保护的。我们不会介绍最后一个,因为我们没有足够的时间,而且在 Unity 开发入门时它并不是很重要。私有变量只能在脚本本身中使用。通常,它们用于存储脚本内部不需要与其他组件共享的数据。
相反,公共变量可以从任何脚本中访问,因此我们需要注意它们的使用位置。当适用时,实现get和set函数是一个好的实践。即使在这本书中我们不会经常使用它们,了解它们是什么也值得学习,你可以在msdn.microsoft.com/en-us/library/w86s7x04.aspx找到相关信息。
此外,公共变量在检查器中是可见的。实际上,仅为了测试目的,你可以创建一个新的脚本并添加以下整数变量:
public int testVariable;
因此,在保存脚本之后,你可以在检查器中设置其值,如下面的截图所示:

因此,为了能够使用公共变量,不需要在脚本中设置它们。通常,我们希望有一个公共变量,因为它将由另一个脚本设置,但在检查器中不可见。在 Unity 中,你可以通过使用属性来实现这一点。
属性
在变量和函数之前,Unity 允许我们插入一个属性。属性被[和]包围,可以包含不同的参数。大约有 30 个属性,它们在功能和用法上确实有所不同。由于我们没有时间逐一介绍它们,我们只会介绍最常用的几个:
-
Header属性格式为[Header("string")]。它在其后的变量之前创建一个标题。以下是将属性添加到我们之前使用的测试脚本变量的示例:[Header("This is a heading")] public int testVariable;结果如下:
![属性]()
-
HideInInspector属性的格式为[HideInInspector]。它隐藏了其后的变量,使其在 Inspector 中不可见。这里是在上一个例子中的使用情况:[HideInInspector] public int testVariable;这是结果:
![属性]()
-
Range属性的格式为[Range(minValue, maxValue)]。它捕捉变量在minValue到maxValue的数值范围内的可能值。再次,我们使用之前的例子:[Range(-10, 10)] public int testVariable;这是我们所得到的结果:
![属性]()
-
最后,
Tooltip属性的格式为[Tooltip("string")]。当光标悬停在变量上时,它会在 Inspector 中创建一个工具提示:[Tooltip("This is a tooltip")] public int testVariable;这里是结果(当光标悬停时):
![属性]()
函数
任何从 MonoBehaviour 类派生的脚本都有两个主要功能:
-
Start(): 这个函数只会在游戏开始时被调用一次。在函数内设置所有变量和获取我们需要的引用很有用。 -
Update(): 这个函数在每一帧都会被调用,需要实时计算一些东西,比如速度或行为。然而,由于这个函数被调用得非常频繁,我们需要注意我们代码内部的内容,以避免游戏运行速度过慢。
还可以实现其他函数,并且它们将被自动调用。实际上,有超过 60 个!
我们稍后会用到其中的一些,所以在这里介绍它们,并在后面的章节中详细探讨:
-
OnTriggerEnter2D(): 当另一个具有碰撞器的对象进入附加到游戏对象的触发碰撞器时,会调用此函数。我们将在处理物理时更好地了解这一点。 -
OnMouseDown(): 如果游戏对象附加了碰撞器,并且玩家在游戏对象上按下鼠标按钮时,会调用此函数。此外,它还可以是一个Coroutine,并且与GUIElements一起工作。 -
OnEnable(): 当对象变为启用和活动状态时,会调用此函数。
你可以在 docs.unity3d.com/ScriptReference/MonoBehaviour.html 上了解更多关于它们的信息。
注释
正如我们在上一章中提到的,游戏开发不是一个容易的过程,因为它涉及许多阶段。因此,记录我们所做的一切非常重要。这并不意味着写一些将要发布的内容,而只是几行代码来提醒自己和团队我们已经完成了什么。实际上,人类的大脑在记住概念方面很出色,但在记住细节方面却不是很好!这在脚本编写阶段是至关重要的。能够阅读几天前自己和团队所写的内容非常重要。在编码时,如果不使用注释,很容易在众多代码行中迷失方向!
由于 Unity 使用 C#编译器,因此可以在代码中插入注释。注释是会被忽略(不会编译)的行,正如其名所示,它们有助于给阅读代码的人留下信息。
在 C#中注释主要有两种方式。第一种是在行首插入//,结果是从该行开始直到下一行之后的所有内容都将被忽略。第二种方式用于多行注释,它由一个开标签/*和一个闭标签*/包围。结果,这两个标签之间的所有内容都将被忽略。以下是一个示例:
*//This line is commented*
This other one is not.
*/*Here it is
again
commented*/*
注意
注释也用于构建自动文档和作为其他内容的标记。然而,这超出了本书的范围。
我强烈建议你始终尝试在你的代码中插入注释,这样以后阅读起来会更方便。为了你的方便,本书附带的所有代码都提供了注释,帮助你理解正在发生的事情以及在哪里。
执行顺序
关于 Unity 中脚本编写的一个重要概念是执行顺序。某些脚本或代码部分在执行其他脚本或代码部分之前执行可能是基本的。结果,这将影响共享资源的修改方式和效率。
标准的顺序如下:
-
编辑器:特别是,
Reset()函数。 -
场景加载:调用
Awake()、OnEnable()和OnLevelWasLoaded()等函数。 -
在第一帧更新之前:如果对象是活动的,将调用脚本中的所有
Start()函数。 -
帧之间:执行
OnApplicationPause()函数。 -
更新:调用所有不同的
Update()函数。 -
渲染:执行特定的渲染功能。
-
协程:它们会一直执行,直到找到 yield 语句。
-
当对象被销毁时:调用
Destroy()函数。 -
退出时:当禁用游戏对象或退出游戏时,将调用要执行的功能。
当然,这只是为了给你一个关于执行顺序的初步印象。如果你对这个主题感兴趣,你可以在官方文档中找到关于执行顺序的详细解释,网址为docs.unity3d.com/Manual/ExecutionOrder.html。
Unity 还提供了在脚本中更改执行顺序的可能性,当你有特定需求时。这可以通过在工具栏中导航到编辑 | 项目设置 | 脚本执行顺序来实现。结果,这个窗口将出现在检查器中,你可以在这里更改顺序:

如需了解更多信息,您可以访问以下链接:docs.unity3d.com/Manual/class-ScriptExecution.html。然而,脚本的执行顺序是为了特定的需求,而我们没有这样的需求,所以我们将不会修改这些设置。不过,我希望这一部分能帮助您更好地理解 Unity 背后的逻辑。话虽如此,我鼓励您在更好地理解 Unity 中的脚本编写时重新阅读这一部分。
制作喷雾
在预制件部分,我们为我们的喷雾投射物创建了预制件。在本节中,我们将了解如何使喷雾在 2D 空间中移动。特别是,我们将学习如何在 Unity 中为 2D 游戏元素创建和使用脚本。
投射物类
由于我们的游戏中可能有不同的投射物可以投向甜食爱好者熊猫,而不仅仅是喷雾,我们需要定义一个通用类。所有不同类型的投射物都将遵循一些通用规则:
-
它们沿直线移动
-
它们携带有关将对敌人造成多少伤害的信息
其中第一个对所有投射物都是相同的,第二个取决于将要发射的具体投射物类型。因此,我们需要创建一个模板。一旦我们将此脚本附加到游戏对象上,我们就可以设置一些变量并调整其行为。在这种情况下,我们想要调整造成的伤害量和速度。
编写投射物基类脚本
因此,首先,让我们创建一个新的脚本,并将其命名为ProjectileScript。接下来,我们需要定义四个变量。第一个是用于伤害量,第二个是速度,第三个是方向,因为我们需要知道投射物将飞向何方。最后一个变量存储其以秒为单位的生存时间。因此,它还与速度变量一起设置了投射物可以到达的距离。实际上,如果投射物没有击中目标,我们不希望它永远直线前进,因为这会消耗计算资源并减慢我们的游戏。因此,在这个持续时间之后,我们需要销毁它。为此,我们可以添加以下四个变量:
public float damage; * //How much damage will the enemy receive*
public float speed = 1f; *//How fast the projectile moves*
public Vector3 direction; *//What direction the projectile is heading*
public float lifeDuration = 10f; *//How long the projectile lives before*
*self-destructing*
接下来要做的事情是在Start()函数中设置一些参数。由于方向将由投掷实体的方向决定,我们无法保证它具有单位范数。因此,我们需要归一化方向。然后,我们需要将我们的投射物图形(在这种情况下是喷雾)旋转到正确的方向。为了实现这一点,我们需要使用Atan2()函数计算角度。在将这个角度转换为度数后,我们使用Quaternion类中的AngleAxis()函数来旋转我们的游戏对象。最后,我们需要在游戏对象被销毁之前设置一个计时器。因此,这将是我们Start()函数的内容:
void Start() {
*//Normalize the direction*
direction = direction.normalized;
*//Fix the rotation*
float angle = Mathf.Atan2(-direction.y, direction.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.AngleAxis(angle, Vector3.forward);
*//Set the timer for self-destruction*
Destroy(gameObject, lifeDuration);
}
在Update()函数中,我们只需要将洒水器移动到那个方向。如果洒水器与某个物体发生碰撞,例如熊猫,这将是我们将在第五章讨论的情况,不再孤单 - 喜欢甜食的大熊猫出击。因此,我们需要根据其方向和速度更新其位置,使其依赖于时间。因此,我们可以编写以下代码:
*// Update the position of the projectile according to time and speed*
void Update() {
transform.position += direction * Time.deltaTime * speed;
}
最后,保存脚本。在这个脚本上还有很多工作要做。例如,方向应该只在x和y上,而不是z,因为它是用于 Z 缓冲区方法。然而,现在它是可以的,我们将在本书稍后讨论所有这些问题。
通过 Prefab 生成的无数洒水器
现在我们已经有一个实现抛物线行为的通用脚本,我们需要实际制作具有这种行为的洒水器的 Prefab。
我们已经为我们的洒水器抛射物创建了一个 Prefab,但我们需要对其进行更改。因此,进入项目面板中的Prefab文件夹,并选择我们的Pink_Sprinkle_Projectile_Prefab。在检查器中,我们需要添加我们刚刚创建的脚本。因此,导航到添加组件 | 脚本 | 抛射物脚本。结果,你应该看到以下内容:

我们需要调整一些值,至少是为了测试目的,这样我们才能看到洒水器在移动。我们可以分配一个方向,例如(1, 1, 0),以及任何值给伤害变量。最后,我们应该得到以下类似的结果:

如果我们按下播放按钮,我们可以看到我们的洒水器按照我们指定的方向前进,并在10秒后被摧毁。
然后,我们需要在 Prefab 上分配Z值。由于它是一个抛射物,我们可以查看我们之前制作的物体所有的Z值表格。因此,我们需要将-2作为值分配。我们还需要将标签分配给我们的抛射物。我们稍后会使用这个标签,但现在最好将其分配给 Prefab。
要分配一个标签,在检查器上,游戏对象名称下方,有一个标签字段,如下面的截图所示:

一旦点击,就会出现一个下拉菜单,其中可以选择不同的标签,包括我们之前定义的两个标签:

我们可以将抛物线标签分配给我们的 Prefab,最终它应该看起来像这样:

这就是我们制造抛射物所需做的所有事情。实际上,我们现在可以在需要时使用这个 Prefab 来生成洒水器。
制作纸杯蛋糕塔
在本节中,我们将了解如何创建我们的塔。这不是一项容易的任务,但通过这样做,我们将获得大量的脚本编写技能。
蛋糕塔的作用
首先,写下我们想要实现的目标,并定义蛋糕塔确切应该做什么是有用的。
最好的方法是写下列表,以便清楚地了解我们想要实现的目标:
-
蛋糕塔能够检测一定范围内的熊猫。
-
蛋糕塔根据其类型对一定范围内的熊猫发射不同类型的弹道。此外,在这个范围内,它使用一种策略来决定射击哪只熊猫。
-
在蛋糕塔能够再次射击之前,有一个装填时间。
-
蛋糕塔可以升级(到一个更大的蛋糕!),提高其统计数据,从而改变其外观。
编写蛋糕塔的脚本
正如我们在上一节中看到的,有许多事情需要实现。让我们先创建一个新的脚本,并将其命名为CupcakeTowerScript。正如我们之前提到的,对于弹道脚本,在本章中,我们实现主要逻辑,但当然,总有改进的空间,正如我们将在本书后面看到的。
射击熊猫
即使我们现在还没有敌人,我们也可以开始编写蛋糕塔射击敌人的行为。在本节中,我们将了解一些关于使用物理检测范围内对象的知识。然而,我们将在第五章中更详细地了解碰撞体,不再孤单——甜食熊猫出击。
让我们先定义四个变量。前三个是公共的,因此我们可以在检查器中设置它们。最后一个变量是私有的,因为我们只需要它来检查已经过去了多少时间。特别是,前三个变量存储我们塔的参数。它们是弹道 Prefab、其范围和其装填时间。我们可以写下以下内容:
public float rangeRadius; *//Maximum distance that the Cupcake Tower
can shoot*
public float reloadTime; *//Time before the Cupcake Tower is able to
shoot again*
public GameObject projectilePrefab; *//Projectile type that is fired
from the Cupcake Tower*
private float elapsedTime; *//Time elapsed from the last time the
Cupcake Tower has shot*
现在,在Update()函数中,我们需要检查是否已经过去了足够的时间以便射击。这可以通过使用一个 if 语句轻松完成。无论如何,在最后,应增加已过时间:
void Update () {
if (elapsedTime >= reloadTime) {
*//Rest of the code*
}
elapsedTime += Time.deltaTime;
}
在if语句中,我们需要重置已过时间,以便在下一次射击时使用。然后,我们需要检查是否在其范围内有某些游戏对象:
if (elapsedTime >= reloadTime) {
* //Reset elapsed Time*
elapsedTime = 0;
*//Find all the gameObjects with a collider within the range of the
Cupcake Tower*
Collider2D[] hitColliders =
Physics2D.OverlapCircleAll(transform.position, rangeRadius);
* //Check if there is at least one gameObject found*
if (hitColliders.Length != 0) {
* //Rest of the code*
}
}
如果范围内有敌人,我们需要决定一个策略,关于塔应该针对哪个敌人。有不同方法来做这件事,塔本身可以选择不同的策略。在这里,我们将实现一个最*的敌人将被针对的策略。不同的策略和策略将在本书的最后一章讨论。
为了实现这项政策,我们需要遍历所有在范围内的游戏对象,检查它们是否实际上是敌人,并使用距离选择最*的一个。为了实现这一点,在之前的 if 语句中写入以下代码:
if (hitColliders.Length != 0) {
*//Loop over all the gameObjects to identify the closest to the
Cupcake Tower*
float min = int.MaxValue;
int index = -1;
for (int i = 0; i < hitColliders.Length; i++) {
if (hitColliders[i].tag == "Enemy") {
float distance =
Vector2.Distance(hitColliders[i].transform.position,
transform.position);
if (distance < min) {
index = i;
min = distance;
}
}
}
if (index == -1)
return;
*//Rest of the code*
}
一旦我们找到目标,我们需要获取塔将投掷投射物的方向。所以,让我们写下这个:
*//Get the direction of the target *
Transform target = hitColliders[index].transform;
Vector2 direction = (target.position - transform.position).normalized;
最后,我们需要实例化一个新的投射物,并将其方向分配给敌人,如下所示:
*//Create the Projectile*
GameObject projectile = GameObject.Instantiate(projectilePrefab,
transform.position, Quaternion.identity) as GameObject;
projectile.GetComponent<ProjectileScript>().direction = direction;
实例化 GameObject 通常很慢,应该避免。然而,出于学习目的,我们可以容忍这一点。在最后一章,我们将看到一些优化技术来消除这种实例化。至于射击敌人,这就完成了。
升级纸杯蛋糕塔,使其更加美味
为了创建一个升级塔的函数,我们首先需要定义一个变量来存储塔的实际等级:
public int upgradeLevel; * //Level of the Cupcake Tower*
然后,我们需要一个包含所有不同升级的 Sprite 的数组,如下所示:
public Sprite[] upgradeSprites; *//Different sprites for the different
levels of the Cupcake Tower*
需要第三个变量来检查纸杯蛋糕塔何时可以升级,因此我们可以添加:
*//Boolean to check if the tower is upgradable*
public bool isUpgradable = true;
最后,我们可以创建我们的升级函数。首先,我们需要检查塔是否可以升级,然后提高它的等级。然后,我们可以根据我们拥有的不同图形的数量来检查,塔是否已经达到最大等级,如果是这样,通过将isUpgradable变量设置为 false 值,阻止玩家再对其进行升级。之后,我们需要升级图形,并提高统计数据。请随意调整这些值,按您喜欢的调整。然而,不要忘记分配新的 Sprite。最后,您应该得到以下类似的内容:
public void Upgrade() {
* //Check if the tower is upgradable*
if (!isUpgradable) {
return;
}
*//Increase the level of the tower*
upgradeLevel++;
* //Check if the tower has reached its last level*
if(upgradeLevel < upgradeSprites.Length) {
isUpgradable = false;
}
* //Increase the stats of the tower*
rangeRadius += 1f;
reloadTime -= 0.5f;
* //Change graphics of the tower*
GetComponent<SpriteRenderer>().sprite = upgradeSprites[upgradeLevel];
}
保存脚本,现在我们对此已经完成了。我们将在本书的后面修改这个函数,但现在让我们为我们的纸杯创建一个预制件。
通过预制件制作的预烘焙纸杯蛋糕塔
就像我们对糖霜所做的那样,我们需要对纸杯蛋糕塔做类似的事情。在项目面板的Prefabs文件夹中,通过右键单击并导航到创建 | 预制件创建一个新的预制件。将其命名为SprinklesCupcakeTower。
现在,将Sprinkles_Cupcake_Tower_0从Graphics/towers文件夹(在cupcake_tower_sheet-01文件内)拖放到场景视图中。通过导航到添加组件 | 脚本 | CupcakeTowerScript将CupcakeTowerScript附加到对象上。检查器应该看起来像以下这样:

我们需要将 Pink_Sprinkle_Projectile_Prefab 分配给 Projectile Prefab 变量。然后,我们需要为升级分配不同的 Sprites。特别是,我们可以使用与之前相同的表格中的 Sprinkles_Cupcake_Tower_*(将 * 替换为纸杯蛋糕塔的等级)。不必过于担心塔的其他参数,例如射程半径或装填时间,因为我们将在稍后看到如何*衡游戏。最后,我们应该看到以下内容:

最后一步是将这个游戏对象拖放到 Prefab 中。结果,我们的纸杯蛋糕塔就准备好了。
更多关于在 Unity 中编码的内容
在我们继续之前,还有一些其他的事情需要学习。
在本书的后面,我们将使用静态变量和继承。这些主题与 C# 比起 Unity 来说更为相关,但如果你想要成为一名优秀的游戏开发者,它们是非常重要的。接下来,你可以找到一个非常简短的说明,但我建议你从 C# 书籍中正确地学习它们。
最后,还有概率,正如我们之前讨论的,它是游戏开发中必不可少的。然而,一旦你掌握了概率的基础,你应该能够在 Unity 中生成随机数。
静态变量
一些类(或组件,在 Unity 的情况下),包含变量,这些变量最终可以在检查器中设置或在运行时更改。然而,整个类可以将其变量共享给所有其实例。这些变量被称为静态变量,并使用 static 关键字声明。一个常见的用途是共享对另一个组件的引用,所有类的实例都应该引用它。我们将在本书稍后更好地了解这一点,当我们使用静态变量时。
继承
如果不同的类在许多方面有共同之处,我们可以给它们一个继承结构。这意味着有一个父类,它包含这些类之间的一般和共同特征,然后它们作为父类的子类来实现。一个常见的例子是为了阐明这一点,想象一下 Fruit、Apple 和 Banana 类。Fruit 是父类,它包含成为水果的所有属性(例如,它是可食用的),子类共享父类的相同特征(因为苹果和香蕉都是水果,因此是可食用的)。然后,它们可以实现该水果的具体特征。例如,苹果是红色的,而香蕉是黄色的。
有时,父类也可以有自己的函数(例如 Eat()),所有子类都可以提供自己的实现。这导致了抽象方法和虚拟方法之间的区别。
抽象方法非常抽象,以至于父类不能为其提供实现,但它的子类必须提供。虚拟方法,相反,可以在父类中实现,因为它提供了通用功能,但子类可以覆盖它以提供更好的实现。
我们将在第七章中更详细地探讨继承,交易蛋糕和终极蛋糕之战 - 游戏玩法编程。
Unity 中的随机数
在 Unity 中生成随机数很容易,因为 Unity 提供了Random类,它允许我们生成随机数。这个类最常用的函数是Range(),它生成一个介于最小值和最大值之间的随机数。以下是一个示例:
*//Generates a random number between 2 and 30*
Random.Range(2, 30);
想了解更多关于Random类的信息,请访问官方文档:docs.unity3d.com/ScriptReference/Random.html。
作业
在本章中,我们看到了如何使用预制体快速复制和克隆 GameObject。所以,在你继续下一章之前,让我们熟练地使用预制体。
以下两个练习是游戏完成时所需的:
-
武装蛋糕塔(第一部分):在
Graphics/projectiles文件夹中的projectiles_sheet_01内部,有九种不同的投射物;然而,我们只使用了其中一种,Pink_Sprinkle。为其他八种预制体创建名称有意义的预制体。别忘了将投射物脚本附加到所有这些预制体上。不必过于担心脚本内部分配的值,例如伤害,因为我们在本书后面的部分将看到游戏的*衡。然而,别忘了分配正确的Z值和适当的标签。 -
武装蛋糕塔(第二部分):在
Graphics/towers文件夹中的cupcake_tower_sheet-01内部,有三个不同的塔及其相应的升级级别;然而,我们只使用了洒糖蛋糕塔。为巧克力和柠檬蛋糕塔创建另外两个预制体。别忘了将蛋糕塔脚本附加到这两个预制体上,并分配相应的投射物和升级级别图形。同样,不必过于担心值,例如范围半径,因为我们在本书后面的部分将看到游戏的*衡。然而,别忘了分配正确的Z值。以下练习将帮助你通过熟悉最佳实践来提高你的技能:
-
为设计师格式化脚本(第一部分):在我们的脚本中,我们使用了许多变量。它们旁边的注释帮助我们理解它们的功能;然而,这些注释对设计师来说是隐藏的。因此,添加工具提示以在检查器中显示是一个好习惯。如果你愿意,可以使用注释的文本作为工具提示的参数。
-
为设计师格式化脚本(第二部分):即使我们在检查器中的脚本看起来不错,如果我们给变量组添加一些标题,它们还可以得到改进。因此,分配有意义的标题并重新排列变量以适应这些标题。
-
为设计师格式化脚本(第三部分):在弹道脚本内部的
direction变量必须设置为公共的,因为它是从纸杯蛋糕塔脚本中改变的。然而,它应该在检查器中可见。使用属性来隐藏它从检查器中。 -
为设计师格式化脚本(第四部分):在我们的脚本中,一些变量最好用滑块来显示,而不是数字输入字段,尤其是那些不能假设为负数的变量。对于这些变量,将它们的显示形式转换为滑块,并禁止它们有负值。
-
最佳实践(第一部分):在纸杯蛋糕塔脚本内部有一个
Upgrade Level变量,它是私有的。然而,创建一个获取函数来检索其值可能是有用的。通过遵循本章变量部分中显示的链接中的指南,实现相应的获取函数。 -
最佳实践(第二部分):再次在纸杯蛋糕塔脚本内部,在
Upgrade()函数中,调用GetComponent()函数来获取SpriteRenderer。最佳实践建议只调用一次这个函数,并将引用存储在一个变量中。创建一个新的变量来存储附加到纸杯蛋糕塔游戏对象的SpriteRenderer组件。通过在Start()函数中使用GetComponent()函数来分配其值。然后,在Upgrade()函数中使用这个变量来更新纸杯蛋糕塔的图形。 -
最佳实践(第三部分):我们没有为纸杯蛋糕塔定义标签,因为我们不需要区分它们。然而,为它们分配一个标签是良好的实践。这是因为将来我们可能需要它,当我们想要扩展我们的游戏时(例如,敌人试图在其限制内避免纸杯蛋糕塔)。因此,创建一个新的纸杯蛋糕塔标签,并将其分配给所有纸杯蛋糕塔 Prefab(应该有三个,包括本节第一个练习中创建的一个)。
摘要
在本章中,我们看到了如何使用 Prefab 快速复制和克隆游戏对象。我们还学习了如何放置 2D 对象,使用 Z-Buffering 方法对它们进行排序,以及如何在 Unity 中设置标签和层。然后,我们创建了一些脚本,并在学习过程中了解到如何用几行代码编写复杂的行为。实际上,我们探索了 Unity 如何处理脚本,它们的主要功能和属性,以及一般的执行顺序。
然而,我们将在本书的后面部分改进本章中编写的脚本,以改善游戏玩法。实际上,本章的目标是熟悉脚本,并记录主要逻辑。因此,改进和调整留待以后。
在下一章中,我们将深入 UI 世界,并将其集成到我们的游戏中。
第三章. 与玩家沟通 - 用户界面
用户界面,通常简称为 UI,在游戏中扮演着至关重要的角色,因为它是与玩家交换信息的主要方式之一。通常,游戏提供有关游戏和玩家的信息,玩家通过 UI 进行输入与游戏互动。
本章将解释为什么 UI 在游戏中扮演如此重要的角色,以及我们在设计和/或实现 UI 时需要考虑的不同事项。当然,本章将专注于 Unity 框架来构建 UI,并解释如何使用它。
在本章的最后部分,我们将从实际的角度出发,通过在我们的游戏 UI 中实现两个重要的游戏元素来了解如何开始 UI 编程。然而,我们将在第五章“The Secret Ingredient Is a Dash of Physics”中看到更多关于 UI 编程的内容,我们将对游戏进行润色,并基于前几章构建的元素实现整个游戏玩法。
最后,你绝对应该考虑购买一本专门关于 UI 的书籍。在撰写这本书之前,我写了一本名为《Unity UI 食谱》的书,由 Packt 出版。它提供了一套现成的完美食谱。在那里,你可以找到这里提到的所有概念以及更多内容,例如不同的技巧和窍门。你可以在www.packtpub.com/game-development/unity-ui-cookbook找到它。
因此,在本章中,我们将学习如何设计和在 Unity 中实现 UI:
-
设计用户界面
-
通过详细查看每个组件来理解 Unity 框架以构建 UI
-
操作和放置 UI 元素
-
为我们的塔防游戏设计 UI
-
在我们的游戏中实现一个生命值条
-
在我们的游戏中实现一个糖量计
就像本书的所有其他章节一样,你将在结尾找到“作业”部分。它包含一系列不同的练习,以提高你的技能并将各种不同的功能实现到你的游戏中。所以,让我们准备好学习很多关于 UI 的知识。
准备工作
我们将使用上一章相同包中的图形来构建我们的 UI。因此,请确保已将其导入,并将图像作为精灵导入,以便在 UI 中使用。
设计用户界面
当你阅读一本书时,文本或图像通常位于页面中央,页码通常位于角落,并且页面是连续编号的。整个过程相当直接且无烦恼。玩家期望获得相同体验,不仅是在游戏玩法上,还包括其他屏幕元素,如用户界面(UI)。设计 UI 需要考虑许多因素。例如,你正在为的*台有局限性,比如屏幕尺寸,以及它能支持哪些类型的交互(是否使用触摸或鼠标指针?)。但界面可能给玩家带来的生理反应也需要考虑,因为玩家将是最终消费者。实际上,还有另一件事需要记住,有些人用母语从右到左阅读,UI 也应该反映这一点。
另一件事需要记住的是,如果你为多个设备设计,尽量保持相同的体验。随着许多应用成为多*台,你不想用户在手机上习惯了某种体验,然后登录到电脑版本,却发现完全不同。因此,在设计 UI 时,确定它在每个设备上的外观。是否在手机上主图标太小,以至于无法理解它是什么?是否导航菜单在桌面版本中太大?确保 UI 优化将确保使用你应用的多设备用户能够无缝过渡,无需再次尝试弄清楚如何访问功能。
就像我们的书籍示例一样,玩家或应用用户习惯于某些约定和格式。例如,房屋图标通常表示主页或主屏幕,电子邮件图标通常表示联系,而指向右方的箭头通常表示将继续到列表中的下一项,或下一个问题,等等。因此,为了提高易用性和导航性,最好坚持这些或至少在设计过程中牢记这些。除此之外,用户如何导航应用也很重要。如果从主页到某个选项只有一种方式,而且需要通过一大堆屏幕,整个体验将会很累人。因此,务必尽早创建导航图,以确定体验各部分的路线。如果一个用户必须通过六个屏幕才能到达某个页面,那么他们不太可能长时间这样做。
响亮的声音可以立即吸引人们的注意。UI 元素也是如此。因此,你想要使更重要的元素成为焦点。关键是要有更少的元素,如果不是只有一个元素,以减少玩家感到不知所措。例如,在游戏中,你可能希望生命条是主要关注的对象。因此,把它放在一个容易被注意到的地方,而不是在玩家的视野角落里。实现这一目标的一种方法是将 UI 元素与环境形成对比,理想情况下是在同一色系内,这样它们就会突出,但不会引起太多的注意而分散玩家的注意力。
一个创建出色配色方案的优秀网站是 Adobe Color (color.adobe.com/)。以下截图展示了它的一个例子:

在说所有这些的时候,不要让设计超过用户体验的实用性。例如,你可能有一个漂亮的用户界面,但它确实使得玩游戏变得非常困难,或者它造成了太多的困惑。尤其是在快节奏的游戏过程中,你不想让玩家不得不筛选二十个不同的屏幕元素来找到他们想要的东西。你希望关卡掌握集中在游戏玩法上,而不是理解用户界面。限制任何时刻存在的 UI 元素数量的另一种方法是有滑动窗口或弹出窗口,其中包含其他 UI 元素。例如,如果你的玩家可以选择解锁许多不同类型的技能,但在游戏过程中任何时刻只能使用其中的一到两个,那么显示所有这些技能是没有意义的。因此,有一个 UI 元素供他们点击,然后显示所有其他技能,他们可以用来替换现有的技能,这是最小化 UI 设计的一种方法。当然,你不想有多个弹出窗口,否则改变游戏中的设置本身就成了一个任务。
用户界面可以提供的一种东西是反馈。反馈不一定是显示在屏幕中央的弹出窗口;它可能只是像仪表增加/减少那样简单,例如健康条,或者玩家的头像随时间变化;例如,随着玩家在游戏中的进展,他们开始变老。关于添加反馈的下一件事是如何让玩家意识到用户界面的变化。在某些情况下,并不一定是要提醒玩家,它只是过程的一个自然部分;在另一些情况下,他们需要知道已经发生或正在发生的变化。向玩家指示变化的一种方式是动画化相关的用户界面元素。这可以像发光效果一样简单;例如,每次玩家被攻击时,他们的健康条都会随着减少而发光或脉动。另一种选择是用声音来指示用户界面中的变化;例如,每次玩家的货币增加时,都会播放一个柔和的铃声。音频可以和动画一样有效,但也要记住,基于音频的反馈并不总是会被听到。例如,有时,如果玩家在公共场所且没有耳机,他们可能会选择禁用声音,所以如果您将声音作为唯一指示玩家用户界面变化的方式,请记住这一点。除了反馈类型之外,每个动作都需要有反应,如果玩家做了他们不应该做的事情,他们需要被告知。所以每次玩家提供输入、点击按钮或与用户界面交互时,都应该发生一些事情。它有多突出取决于您;只需确保它清晰且不过分即可。
本节为您介绍了设计良好用户界面背后的问题。有许多其他因素需要考虑,以至于专门关于用户界面的书籍都已被撰写。例如,其中之一就是本地化,这是一个如此庞大的概念,以至于需要整整一章来描述它(您可以在第八章中找到关于本地化的简要介绍,“蛋糕之外是什么?”)。
然而,在继续之前,我想指出一个关于用户界面的技术术语,因为它也出现在 Unity 的官方文档中。有些用户界面并不是固定在屏幕上的,而是在游戏环境中有一个实际的空间。一些设计师将这些用户界面称为“diegetic”,与传统的“non-diegetic”界面相对。这是一个从其他学科借用的术语,因此并不是所有关于用户界面的说法都被普遍接受。事实上,它甚至可能引起一些混淆。为了给您提供更多历史背景,这个术语来源于单词“diegesis”。我们可以在 Merriam-Webster 词典中看到它的定义:
“由叙述者讲述的故事,叙述者总结情节事件并对角色的对话、思想等进行评论。”
一个叙事界面或简化地说,放置在游戏世界中的 UI 的例子是游戏《死亡空间》(Visceral Games,前 EA Redwood Shores)中的小地图,我们可以在下面的屏幕截图中看到:

编程用户界面
如前文所述,设计用户界面可能是一项艰巨的任务,需要经验才能掌握,尤其是当你考虑到所有应该考虑的元素时,例如受众的心理。然而,这仅仅是开始的一半。实际上,设计是一回事,让它有效运行则是另一回事。通常,在大团队中,有艺术家负责设计 UI,而程序员则负责实现它。
UI 编程真的有那么不同吗?答案是:不,编程还是编程;然而,它是编程领域中的一个相当有趣的分支。如果你是从零开始构建你的游戏引擎,实现一个处理输入的整个系统并不是几个小时就能完成的。捕捉玩家在游戏和 UI 中进行的所有事件并不容易实现,需要大量的实践。幸运的是,在 Unity 的背景下,大多数 UI 的后端工作已经完成。此外,正如我们将在下一节中看到的,Unity 提供了一个稳固的框架,用于处理 UI 的前端工作。这个框架包括不同的组件,可以很容易地使用,无需了解任何编程知识。但如果我们真的对解锁 Unity 框架在 UI 方面的潜力感兴趣,我们不仅需要理解它,还需要在其中进行编程。
即使有一个稳固的框架,比如 Unity 中的那个,UI 编程仍然需要考虑许多因素,足以在大型团队中为这个角色设定一个特定的位置。精确地实现设计师的意图,同时尽可能不牺牲性能,是 UI 程序员(至少在使用 Unity 时)的主要工作。
掌握这些技能需要时间和耐心,而本章只是对这个领域的入门介绍。希望你会喜欢阅读这一章。
用户界面系统
现在我们已经对如何设计 UI 有了基本的了解,是时候学习 Unity 如何处理所有这些内容了。从 Unity 4.6 版本开始,可以使用一个专门的 UI 系统,称为 UI。本节的目标是理解它是如何工作的。
注意
值得注意的是,在版本 4.6 之前,Unity 使用了一个名为 GUI 的旧系统。最*,Unity 开发者并没有完全摒弃这个系统,而是稍作修改并将其更名为IMGUI,即Immediate Mode GUI。正如我们可以在官方文档中看到的那样,它并不打算用于游戏,而是供程序员快速调试使用。此外,它还用于在通过脚本扩展 Unity 时创建窗口或自定义检查器,这是一个肯定超出了本书介绍范围的话题。然而,如果你对了解更多内容感兴趣,我邀请你阅读官方文档:docs.unity3d.com/Manual/ExtendingTheEditor.html。同时,也可以观看这个视频教程:unity3d.com/learn/tutorials/topics/interface-essentials/building-custom-inspector?playlist=17090。
然而,你可能觉得这一部分有点像百科全书。实际上,你可以将这一部分作为所需 UI 元素的快速参考。因此,如果你已经对 Unity UI 有一定的了解,可以自由地跳过其中的一部分。
此外,这一部分的结构是这样的,基本内容在开头,然后逐渐处理更复杂的话题,直到超出本书的范围。实际上,当你达到这个水*,真正的问题只是微调或实现一个非常具体的效果时,这一部分将给你一个如何工作的总体概念,以便从那里进一步扩展你的知识,特别是通过官方文档。
画布
如果你想知道什么是画布,让我们先从一些背景信息开始。根据 Merriam-Webster 词典,画布被认为是以下内容:
"一种紧密编织的布料,通常由亚麻、大麻或棉花制成,用于服装,以前常用于帐篷和帆布。"
但可能最接* Unity 所定义的画布定义是这样的,同样来自 Merriam-Webster 词典:
"一块背面或框架作为绘画表面的布料;也指这样的表面上的绘画。"
在计算机图形学中,画布的含义略有不同,我们可以在维基百科上看到其定义:
"在计算机科学和可视化中,画布是一个容器,包含各种绘图元素(线条、形状、文本、包含其他元素的框架等)。它得名于视觉艺术中使用的画布。有时也称为场景图,因为它安排用户界面或图形场景的逻辑表示。某些实现还定义了空间表示,并允许用户通过图形用户界面与元素交互。"
在 Unity 中,画布是 UI 系统的一个重要组件。由于 UI 在 Unity 中内部渲染的方式与场景中的其他部分不同,我们需要指定场景中哪些元素属于 UI。特别是,所有 UI 元素都应该是一个带有画布组件的游戏对象的子元素。我们可以通过在层次结构面板上右键单击并导航到UI | 画布来创建一个画布。
因此,在我们的场景中将会创建两个对象,如下面的截图所示:

然而,现在让我们只关注画布,将事件系统留到以后再讨论。
注意
创建一个新的 UI 元素,例如一个图像,可以通过在层次结构上右键单击然后选择UI | 图像来实现,如果场景中还没有画布,则会自动创建一个画布(以及一个事件系统)。这个 UI 元素被创建为该画布的子元素。
场景中的画布以白色矩形的形式呈现。这有助于我们在不切换到游戏视图的情况下编辑 UI。
如果场景视图未设置为 2D,例如我们在 3D 游戏中工作,画布将出现扭曲,如下所示:

由于 UI 将以正交方式渲染,处理 UI 的一个经验法则是将场景视图切换到 2D(如我们在第一章中学习的),如下所示:

如果我们选择画布,我们应该能在检查器中看到以下内容:

主要设置是渲染模式,它允许我们指定我们打算如何使用我们的 UI。让我们更仔细地看看这些选项。
屏幕空间 - 覆盖
这种渲染模式是最常用的。实际上,它通过在场景上渲染 UI 元素来将 UI 元素放置在屏幕上,就像许多游戏一样。这意味着 UI 元素被完美地正交渲染。
此外,如果屏幕分辨率改变或被调整大小,画布也会改变大小以自动适应新的比例。
屏幕空间 - 相机
相反,这种渲染模式与一个特定的相机相关联,UI 将渲染在这个相机之上。这意味着画布被放置在所选相机前方的一个给定距离处。因此,UI 元素将受到所有相机参数和效果的影响。这包括如果相机设置为透视时的透视失真,这是由相机视野参数控制的。
与之前一样,画布的大小可能会根据屏幕分辨率和相机视锥体进行调整。
如果你想知道相机视锥体是什么,请继续阅读这个信息框。当你选择一个相机时,你会看到一个截顶的棱锥体(如果相机处于透视模式,否则它具有*行六面体的形状),就像下面截图中的那样:

这就是相机视锥体,其中所有内容都将由相机渲染。离相机较*的*面称为*裁剪面,而较远的称为远裁剪面。理解透视相机为什么有这种形状并不难,但这超出了本书的范围。然而,在文档中,你可以找到两个有趣的页面,讨论了这一点。它们很容易理解,为了你的方便,这里提供了链接:docs.unity3d.com/Manual/UnderstandingFrustum.html 和 docs.unity3d.com/Manual/FrustumSizeAtDistance.html。
世界空间
这种渲染模式将使画布表现得像场景中的任何其他游戏对象。这意味着画布在世界上有一个精确的位置(因此得名,因为它放置在世界空间中)。因此,UI 可能会被场景中的其他对象遮挡。其尺寸是通过 Rect Transform 组件手动设置的(见下文的一些段落)。这种模式对于打算成为世界一部分的 UI 很有用。正如我们之前讨论的,这也被称为叙事界面。
UI 元素的绘制顺序
正如我们将在下一段落中看到的,UI 元素将是屏幕上渲染的图像或文本。然而,当两个这些组件重叠时会发生什么?有一个顺序,所以画布中的 UI 元素将以它们在层次结构中出现的顺序绘制。这意味着第一个子元素首先绘制,然后是第二个子元素,依此类推。因此,当两个 UI 元素重叠时,在层次结构中较晚的那个将出现在较早的那个之上。
要改变哪个元素显示在其他元素之上,只需通过拖动它们在层次结构中重新排序元素即可。
为了更好地理解这一点,请看下面的截图,它代表了两种不同情况下的两个图像。为了你的方便,相应的层次结构面板被叠加在图像旁边。

柠檬纸杯蛋糕被渲染在巧克力纸杯蛋糕之上,因为柠檬纸杯蛋糕是画布的最后一个子元素
顺序也可以通过在变换组件上使用这些方法从脚本中控制:
-
SetAsFirstSibling(): 将游戏对象放置为其父对象的第一个子元素。因此,它将首先渲染(相对于其兄弟),因此将被发送到后面(所有其他 UI 元素都将位于其上方)。
-
SetAsLastSibling():将游戏对象放置为其父对象的最后一个子对象。因此,它将在渲染时最后(相对于其兄弟),因此被带到前面(所有其他 UI 元素都将位于其后)。
-
SetSiblingIndex():将游戏对象放置到特定索引,允许决定该游戏对象将在渲染层次结构中的哪个位置。
视觉组件
Unity UI 自带不同的预置组件来构建我们的 UI。最常用的组件是视觉组件,它允许在屏幕上渲染自定义内容。
图像组件
图像组件,正如其名所示,允许我们在屏幕上渲染图像。实际上,我们需要指定一个源图像,这是我们想要渲染的图像。以下截图是一个例子:

注意
至于精灵,我们打算在项目中用于 UI 的图像资源必须设置为精灵(2D 和 UI),正如第一章 Unity 中的*面世界 所解释的。
然后,我们可以调整颜色,这是精灵的乘数,以及如果我们需要的话,分配一个材质。
一旦设置了源图像,我们可以通过选择图像类型来定义精灵的外观。选项如下:
-
简单:只是均匀地缩放图像或精灵。
-
切片:如果精灵已经被 9 切片(正如第一章 Unity 中的*面世界 所解释),图像的九个不同部分将被不同地缩放。
-
*铺:这与上一个类似,但 9 宫格的中心部分是*铺而不是拉伸。
-
填充:这与简单类似,但允许我们显示图像的一部分,就像它被填充一样。这由填充的起点、方法和数量等参数控制。我们将在本章后面使用这个功能,并会发现它对于创建视频游戏中的条形图非常有用:

注意
一些高级条形图,如《王国之心》风格的血量条,可以在之前提到的书中找到:Unity UI CookBook,Packt publishing 在第二章末尾的 Implementing Counters and Health Bars。
此外,当图像是简单或填充时,设置原生大小按钮可见。它只是恢复图像的原始大小。当你分配一个新的源图像时,这非常有用,你可以使用此按钮恢复原始比例,然后再将其缩放到适合 UI 的正确大小。
文本组件
文本组件,正如其名所示,允许我们在屏幕上渲染任何文本。有时,在某些书中,它被称为标签,因为它通常用于给其他 UI 组件添加标签。以下截图是一个例子:

它包含一个文本区域,可以使用矩形工具(见下一段)进行扩展。在组件中,你可以找到所有基本的文本转换,例如设置字体、字体样式和字体大小。此外,你可以启用或禁用富文本功能,默认情况下是启用的。
注意
如果你想知道什么是富文本功能,请继续阅读这个信息框。
富文本功能允许我们在文本中放置一些 HTML 标签,以仅更改文本的特定部分。用非技术术语来说,你可以改变单个单词的颜色或将其字体样式改为斜体。例如,你可以在文本区域中看到像这本书真的很棒这样的内容,但在文本组件中,它被写成This book <b>is really</b> amazing。
由于它们是 HTML 标签,它们必须放置在开始处,指定设置,并在你想要应用更改的文本部分的末尾。
这些是 Unity 支持的主要标签:
<b>这些标签之间的文本将显示为粗体</b>
<i>这些标签之间的文本将显示为斜体</i>
<size=50>这些标签之间的文本大小将为 50,你可以将数字更改为任何数字</size>
<color= #rrggbbaa>这些标签之间的文本将使用开头指定的十六进制颜色进行着色</color>
如果你不知道什么是十六进制颜色,它只是一个十六进制数值(因此,它还包含一些字母),代表一种颜色;你可以在维基百科页面这里了解更多:en.wikipedia.org/wiki/Web_colors。
然而,请记住,你不需要了解所有这些颜色背后的详细理论就可以使用这些颜色。实际上,有很多在线颜色选择器可以为你提供特定颜色的十六进制数值。然后,你只需将代码复制并粘贴到 Unity 中的文本中即可。此外,你不必一定使用十六进制代码来指定颜色,Unity 中还有一些预设。实际上,你只需使用<color=red>word</color>这样的代码来使单词变红,而不必指定整个十六进制代码。所有这些颜色快捷方式的列表可以通过点击此信息框末尾的链接找到。另外,你也可以从在线工具中选择十六进制颜色,例如:www.w3schools.com/colors/colors_picker.asp 或 htmlcolorcodes.com。
有一些特殊的标签,如材质和四边形,它们有非常特定的用途。如果你想了解更多,请点击此信息框末尾的链接。
这些标签的另一个酷炫功能是你可以嵌套它们!这意味着你可以同时使用多个标签。例如,你可以有部分文本是蓝色、粗体和斜体的。然而,它们必须以相反的顺序关闭,否则它们将不起作用。
如果您想了解更多信息,请点击此链接:docs.unity3d.com/Manual/StyledText.html。
此外,您还可以找到更改文本对齐方式以及垂直和水*溢出的选项,这意味着控制当文本大于文本区域时会发生什么。最佳拟合选项将文本缩放以适应文本区域的可用空间。
基本变换
我们已经看到了一些基本的 UI 元素,但我们如何放置和操作它们?在下一节中,我们将学习各种不同的实现变换的形式。
矩形工具
由于 UI 元素类似于精灵(两者都是 2D),快速操作它们最好的方式是使用矩形工具。
快速回顾,您可以在 Unity 编辑器的左上角找到矩形工具,它是右侧的最后一个,如下所示:

在我们的精灵或 UI 元素周围应该出现一个边框。因此,我们可以以下列方式对其进行转换:
- 如果我们在矩形内点击并拖动,我们可以移动对象,如下面的图片所示(为了学习目的,在图像组件中使用了一个美味的纸杯蛋糕):

- 如果我们点击中间的蓝色点,即旋转点,我们可以改变其位置(在这本书中,我们不会改变任何旋转点,因为我们不需要这样做):

- 如果我们点击并拖动一个边缘,我们可以沿该方向缩放,就像我们在这里看到的那样:

- 如果我们点击并拖动一个角,则可以沿两个方向自由缩放。此外,如果您在拖动时按住Shift键,则缩放将是均匀的,这意味着它将在两个轴上以相同的数量增加大小,同时保持对象的比率不变:

- 最后,如果我们把光标放在矩形外的角上,即矩形外面,会出现一个小旋转图标。通过点击并拖动它,可以绕旋转点旋转对象:

这就是关于矩形工具的所有内容。
矩形变换
Unity 处理精灵和 UI 元素的方式有很大的不同。实际上,精灵有通常的变换组件,用来表示位置、旋转和缩放。相反,UI 元素有一个矩形变换(2D 布局的对应物),它更复杂并存储更多信息。实际上,变换表示空间中的一个点,而矩形变换表示一个 UI 元素可以放置的矩形。以下截图显示了这一点:

小贴士
Unity 在每一帧结束时执行所有不同矩形变换位置的计算,以确保相对于帧的其余部分具有最新的值。因此,当您使用 Start() 函数时,矩形变换的这些值可能不正确。为了解决这个问题,您可以通过调用 Canvas.ForceUpdateCanvases() 函数强制更新画布。
此外,如果矩形变换的父元素也是一个矩形变换,子矩形变换也可以指定它相对于父矩形的定位和大小。这种层次结构使得矩形变换非常强大,尤其是在您需要为多个分辨率设计时。
除了缩放,矩形变换还可以调整大小。这些操作类似,但区别在于调整大小保持局部缩放不变,并改变高度和宽度。因此,字体大小、切片图像的边框等不会受到调整大小的影响,而在缩放的情况下则会受到影响。
与 2D 精灵类似,矩形变换通过 UI 元素的锚点应用缩放、旋转和调整大小。然而,您可以直接在场景视图中通过拖动它(小蓝色圆圈)在 UI 元素内直接更改它。
关于这个组件最重要的概念之一是锚点,它允许我们指定 UI 元素相对于画布及其父元素的关系。它们在场景视图中显示为四个小三角形手柄。与这些锚点相关的信息在检查器中的矩形变换组件中显示。
很遗憾,没有简单的方法可以解释锚点,除非看到它们在动态中的效果,比如在视频或动画 gif 中。由于这是一本书,它无法包含这样的动画媒体,这会立即向您阐明这个概念。因此,为了避免在复杂的锚点解释上浪费时间,这些解释可能并不完全理解,我邀请您访问官方文档,在这里的锚点部分:docs.unity3d.com/Manual/UIBasicLayout.html。不用担心,您从网页回来时我还在这里。
如果您已经阅读了网页,除了看到动画 gif,您还看到了锚点预设,如下面的截图所示:

这些是快速正确锚定 UI 元素的快捷方式。当然,我邀请您在需要时手动更改游戏中此配置。
布局组件
在上一节中,我们看到了如何将 UI 元素放置在屏幕上。然而,有时,在满足一定标准的情况下自动将它们放置在屏幕上非常有用,尤其是在不知道 UI 元素的数量,并且运行时发生变化的情况下。这可以通过手动脚本实现,但 Unity 提供了一系列布局组件,有助于基本的布局定位。
自动布局系统由两种不同类型的元素组成:布局元素和布局控制器。为了理解前者,请注意,每个具有 Rect Transform 的游戏对象,以及最终的其他组件,都是一个布局元素。这些类型对它们应该有多大有一定的了解,但它们并不直接控制它。相反,布局控制器是控制一个或多个布局元素的大小和位置的组件。它们可以控制自己的布局元素或它们附加的游戏对象的子布局元素。
小贴士
布局控制器以难以恢复先前状态的方式更改 Rect Transform。因此,在添加布局控制器和/或修改之前,请确保处于播放模式,以便在不造成任何不希望的布局更改的情况下进行更改。一旦您对更改满意,停止播放模式,并插入您找到的适合您需求的值。
布局控制器分为适配器和布局组。
Fitters
Fitters 只控制其自身布局元素的大小。在调整 UI 元素大小时,请记住,这是围绕我们之前讨论的枢轴点发生的。因此,您也可以使用它来对齐 UI 元素。例如,如果枢轴点位于中心,则元素将在所有方向上均匀缩放,而如果它放置在角落,例如左上角,则元素将向右下方缩放。所有其他位置都会在元素缩放的四条方向上给出不同的权重。
说到这里,让我们来看看 Fitters 控制器:
- 内容大小适配器:控制其自身布局元素的大小。大小由游戏对象上的布局元素组件提供的最小大小或首选大小确定。这样的布局元素可以是图像或文本组件、布局组或布局元素组件:

- 宽高比适配器:可以调整高度以适应宽度或反之亦然,或者它可以使元素适应其父元素或包围其父元素。宽高比适配器不考虑布局信息,例如最小大小和首选大小:

布局组
相反,布局组控制其子元素的布局元素,而不是它们自己的。它们用于有序地放置 UI 元素。它们有不同的选项来控制子元素之间的间距,并定义首选的高度和/或宽度。其他选项包括强制子元素扩展以适应可用空间的可能性,或者决定当它们大于可用空间时会发生什么。它们如下所示:
- 垂直布局组:允许我们将子元素沿着垂直轴堆叠,并将它们堆叠在一起:

- 水*布局组:允许我们将子元素沿着水*轴堆叠,并将它们并排放置:

- 网格布局组:这允许我们将子元素在网格中堆叠,垂直和水*都可以:

布局元素组件
还有一个组件,即 布局元素 组件。正如其名所示,它不是一个控制器,而是允许我们从 Rect Transform 中更改布局元素设置。实际上,当放置在布局元素上时,它允许我们覆盖设置,例如最小值、首选值和弹性,对于高度和宽度都是如此。此外,它有一个标志来忽略控制器。所以,想象一下在网格布局组件内部有一个标签,你不想让标签与其他所有元素一起堆叠在网格中,而是放在顶部,定义网格的内容。在这种情况下,忽略控制器对于将标签放置在网格外部,同时仍然是网格的子元素非常有用,这样就可以作为一个独特的块移动它,而无需每次都替换标签。
这就是该组件的外观:

交互组件
Unity UI 比视觉组件部分提供了更强大的预制组件。实际上,有许多用户可以与之交互的组件。这些交互可以是鼠标或触摸/点击事件,也可以是键盘或控制器事件。
然而,这些组件本身是不可见的,并且必须与一个或多个视觉组件结合使用才能正确工作。
可选择基类
在了解单个交互组件的工作方式之前,我们需要了解一些所有这些组件共有的基本设置。特别是,这些设置来自 Selectable 基类,它具有过渡和导航选项。
交互选项
这只是一个标志,用于确定交互组件是否启用交互。当勾选时,交互组件将处于 禁用 状态(见下一节)。
过渡选项
通常,交互组件需要向玩家发送一些反馈,以便他们可以理解动作是否已执行。
在这个 Unity 实现中,交互组件可能有四种状态。它们如下:
-
Normal: 交互组件未被触摸
-
Highlighted: 当指针在交互组件上,但尚未执行点击(或触摸屏上的触摸/轻触)时
-
Pressed: 当点击(或触摸/轻触)发生在交互组件上时
-
Disable: 当交互组件不可交互时
这些过渡和这些状态的规定可以通过四种不同的方式发生:
- None: 交互组件不会改变状态。当我们需要以自定义方式实现与组件的交互时,这非常有用:

- Color Tint: 此选项默认选中,并为前面的每个状态定义了一种颜色色调。此外,它包含一个淡入淡出持续时间来调节组件从一种颜色变为另一种颜色的速度,以及一个颜色乘数。因此,交互组件将为每个状态*滑地改变颜色:

- Sprite Swap: 通常,交互组件还附有一个图像组件,它定义了基本图形。在此过渡模式中,不是改变颜色,而是每个状态都有一个不同的 Sprite。当您为每个状态都有自定义图形时,这非常有用:

- Animation: 这是最高效的过渡模式,因为它允许您为每个状态自定义动画(我们将在接下来的章节中更多地讨论动画):

导航选项
导航选项决定了玩家在游戏模式期间如何导航 UI 元素。以下是可用的不同选项:
-
None: 无键盘导航。这在您想在游戏中实现自己的导航系统时非常有用。此外,在此模式下设置的交互组件不会因点击(或轻触)而获得焦点。
-
Horizontal: 水*导航。
-
Vertical: 垂直导航。
-
Automatic: Unity 将尝试根据 UI 元素的位置猜测正确的导航。
-
Explicit: 在此模式下,您可以指定每个箭头键要选择的下一个 UI 元素。这允许精细的导航控制:

此外,可视化按钮允许在场景视图中可视化导航方案。以下截图展示了这种可视化的一个例子:

按钮图
这是任何游戏中都可以找到的经典交互组件。它只包含一个事件,OnClick(),当按钮被点击/轻触时触发。当然,您可以将任何动作链接到事件。以下截图是一个示例:

注意
请记住,如果指针在点击/轻触释放之前移离按钮,则动作不会执行。您可以在作业部分找到一个关于这个的练习。
切换和切换组
切换组件允许玩家关闭或打开一个选项。以下截图是一个示例:

如按钮一样,切换有一个单一的事件OnValueChanged(),每次切换改变其状态时都会调用;新状态值作为事件数据中的布尔参数传递(见下一段)。此组件与另一个称为切换组的组件配合良好,该组件控制是否只有一个选项在切换组中被打开,如下面的截图所示:

您可以通过将其添加到所有希望加入组的切换的组属性中,来设置一个切换组。例如,这对于互斥选择非常有用,如角色或职业选择。其他常见用途是调整游戏设置,如游戏速度、难度或配色方案。当然,您可以在场景中同时使用多个切换组;然而,一个切换只能属于一个组。
滑块
如其名所示,滑块是一个带有手柄的条,手柄可以从条的开始处滑动,即被认为是最小值,到条尾,即被认为是最大值。条中间的所有值都与手柄在条上的位置成比例。默认情况下,手柄从左到右增加其值,但通过调整方向属性,可以改变其他方向,不仅可以从右到左,还可以沿垂直轴。以下截图是一个示例:

滑块有一个单一的事件,OnValueChanged(),当滑块的手柄被拖动时触发,并将滑块的新值作为浮点数传递给触发动作。
滚动条
这个组件与滑块非常相似,因为它在一条条上有一个手柄,最小值始终是 0.0,最大值是 1.0。所有介于两者之间的值代表手柄将所在的不同百分比。同样,滚动条可以通过调整方向属性来定位。以下截图是一个示例:

与滑块的区别在于,根据某些内容(如文本区域)可以拉伸滚动条的把手。当文本增加时,把手会变小,以便在更多内容之间滑动,这代表了可滚动的量。另一方面,当内容不大时,把手会增加其尺寸以完全填充条形,不允许滚动。
滚动条有一个单独的事件,称为OnValueChanged(),它的工作方式与滑块上的同名列事件完全相同。
下拉菜单
下拉菜单是 Unity UI 中相对较新的组件,因为它自 Unity 5.2 以来就已经发布/实现了。这个组件允许玩家从一系列选项中选择。组件只显示当前选中的选项,当玩家点击/轻触它时,完整的列表出现。一旦从列表中选择了另一个项目,列表就会关闭,新项目被选中。此外,如果玩家在其他组件外部点击,他或她可以关闭列表而不更改项目。以下截图是一个例子:

使用检查器中的下拉菜单非常直观,即使你需要熟悉层次结构中的模板才能改变其外观。不幸的是,我们没有时间详细说明这个组件的工作原理,但我相信通过访问官方文档你可以轻松理解它:docs.unity3d.com/Manual/script-Dropdown.html。
无论如何,我们不会在我们的游戏中使用这个组件,但在作业部分,你可以挑战自己更好地理解这个组件。
输入字段
输入字段组件允许玩家在游戏中输入文本,具体来说是在文本区域内。当然,你需要与文本组件或其他视觉元素一起使用它。以下截图是一个例子:

注意
输入字段也可以添加到已经存在的文本组件中,使其可编辑。为此,在层次结构面板中选择文本组件,然后在检查器中导航到添加组件 | UI | 输入字段。然后,将文本组件(游戏对象本身也可以)拖放到输入字段的文本组件变量中。此外,你可能还想添加一个占位符。因此,我建议你创建一个输入字段并研究它与其原始结构一起如何工作,然后添加现有的文本区域。
当玩家在输入时,文本组件的文本属性将改变,并且可以从脚本中检索。
此外,输入字段有不同的选项来定义允许的字符类型,是否应该被屏蔽(例如,如果是一个密码或 PIN),是否有数量限制,或者是否允许多行编辑。你可以在官方文档中了解更多关于这些附加功能的信息:docs.unity3d.com/Manual/script-InputField.html。
输入字段组件有两个事件:OnValueChanged(),每次玩家输入时都会触发,以及OnEndEdit(),只有当玩家停止输入时才会触发。在这两种情况下,整个文本组件中的文本都会通过字符串参数传递给动作函数。
请记住,富文本默认是关闭的。你可以启用它,但对于输入字段来说,它并不很好地支持,因为文本中的导航包括标记,而视觉上没有。因此,对于将要输入的人来说,这真的很令人困惑。通常,你不需要富文本功能来编辑文本;因此,作为一个经验法则,只需保持富文本关闭。
Scroll Rect
当你的内容比它应该占据的区域大时,这个组件会被使用。Scroll Rect 允许我们在一个矩形内使内容可滚动,并在相对较小的区域内显示所有内容。通常,这个组件会与遮罩组件一起使用;这样,矩形外的所有内容都将不可见,你将实现一个滚动视图。以下截图是一个示例:

此外,你可以分配滚动条(水*和垂直轴上的滚动条)以轻松滚动内容。你可以在组件中找到更多可调整的选项,如果你真的想了解它们,我邀请你查阅官方文档:docs.unity3d.com/Manual/script-ScrollRect.html。
最后,Scroll Rect 只有一个事件,OnValueChanged(),当 Scroll Rect 的位置改变时触发,这表示玩家已经滚动。
更多关于 UI 渲染的信息
我们已经看到了很多关于 UI 的内容,但这并不是全部。本节介绍了 Unity UI 中的一些相对高级主题。你可以自由地跳过这一节,或者在不集中注意力的情况下阅读,以便完全理解所写的内容,你总是可以稍后回来。
画布渲染器
仔细的读者已经注意到,在所有 UI 元素中,总是附有一个 Canvas Renderer,如下面的截图所示:

这个组件为什么不允许我们更改任何选项呢?它允许 Unity 知道特定的 UI 元素应该在 Canvas 中渲染。在非常特殊的情况下,当 UI 元素是从头开始构建时,我们需要手动添加这个组件。然而,如果您不打算从头开始构建自定义 UI 元素,只是使用 Unity 提供的那些(这些足以构建非常复杂的 UI),您可以忽略这个组件。实际上,每次我们创建一个 UI 元素时,它都会自动创建。
即使Canvas Renderer在检查器中没有选项,它也有一些可以通过脚本访问的属性。对于这些函数和变量,您可以在以下位置找到详细信息:docs.unity3d.com/ScriptReference/CanvasRenderer.html。
更多视觉组件
我们已经分析了主要视觉组件;然而,还有更多视觉组件,它们在特殊情况下很少使用:

最常见的是遮罩组件。它与Scroll Rect一起用于创建滚动视图。它强制子元素具有父元素的形状。然而,它不支持 alpha 通道。这意味着子元素的一部分将是可见的或不可见的,而无需在遮罩中指定任何类型的透明度。
注意
如果您想知道为什么不支持 alpha 通道,我们需要提及 Unity 下面遮罩的实现。
当您使用 GPU 编程时,您被限制只能使用某些缓冲区来渲染事物。现代 GPU 有一个名为 Stencil Buffer 的缓冲区,它与Color Buffer和Depth Buffer一起使用,它只能假设整数值并在像素级别上工作。通常,它用于避免渲染屏幕的某些部分,并提高整体性能,Unity 也是如此。这种高级使用可能包括根据深度缓冲区动态更改它。然而,Unity 只是使用这个缓冲区来不渲染遮罩未覆盖的屏幕部分,特别是将值 1 分配给应该渲染的像素。
此外,Unity 允许嵌套遮罩,特别是使用 AND(&)操作符对它们进行操作。结果,只有当像素位于所有嵌套遮罩内时,像素才会被渲染。您可以通过将不同的纸张遮罩重叠到绘图上来轻松想象这一点,只有当所有纸张遮罩重叠的部分才是可见的。
最*,Unity 还引入了另一种类型的遮罩,Rect Mask 2D:

与之前的遮罩组件相比,它有一些限制,例如它仅在 2D 环境和共面元素(非共面仍然可能,但组件可能无法按预期工作)。然而,这种方法带来了一些优点,例如不需要使用模板缓冲区(参见前面的信息框),从而带来性能提升,因为没有额外的绘制调用。

另一个非常特殊的视觉组件是原始图像。它在某些方面与图像组件有限制,但也有一些其他功能。实际上,原始图像没有动画图像的选项。然而,它直接与字节工作,正如其名称所暗示的。因此,它不仅能够显示图像作为精灵,还能显示纹理。为了理解这有什么用,想象一下纹理只是一个字节数组,并且它们可以在运行时更改。这意味着你可以在运行时从 URL 下载纹理,并在原始图像中显示它。其他用途可能包括使用渲染纹理(从 Unity 5.x 开始,它们也适用于个人版,而不仅仅是专业版),并流式传输游戏世界中另一个相机所看到的画面。这可能被用来快速创建游戏中的小地图。你可以在本章开头建议的Unity UI 食谱的最后一章中找到此过程的详细描述。关于原始图像还有一点:它们有一个UV 矩形选项。这意味着你可以按需缩放和缩放图像/纹理,而不会改变纹理本身。
UI 效果组件
除了视觉组件和交互组件之外,Unity 还有一类特殊的组件,称为效果组件。在一些书籍和文档中,你可能将它们视为视觉组件的子类。
这些组件如下:
- 阴影:这允许我们为图像或文本组件添加阴影效果。它必须附加到文本或图像组件相同的游戏对象上。其选项改变阴影的距离和颜色。此外,在图像的情况下,一个布尔值控制创建阴影的组件是否也应该使用图像的 alpha 通道:

- 轮廓:这与阴影组件的工作方式类似,但不是添加阴影,而是添加轮廓。控制选项与阴影组件相同:

- 作为 UV1 的位置:当这个选项在图像组件上时,Unity 会将画布位置传递到第一个 UV 通道。这意味着如果你有一个自定义着色器,你可以使用这个功能来创建折射或 UV 偏移采样。
UI 和灯光
除了我们已经发现的关于 UI 的所有内容之外,您还可以在 UI 上使用灯光。它们用于使 UI 感觉更真实,尤其是在 UI 放置在 3D 世界中,或者当它具有某种透视效果时。然而,请记住,添加灯光可能会降低性能。
如果 UI 在 3D 世界中,你可能希望它受到世界光线的影响,但如果它有透视效果,比如画布的 Screen Space - Camera 的情况,你可能只想让它受到某些光线的影响。因此,你需要创建一些图层来过滤哪些光线会影响 UI。
然而,将灯光放置在场景中以使 UI 组件受到光照影响并不那么简单,因为您需要一个对光照有响应的材料。在这种情况下,Unity 为 UI 提供了特定的着色器。无论如何,这超出了本书的范围,因为它更多地涉及 3D 游戏开发,而不是 2D。但如果您想了解更多,在 Unity UI 烹饪书 的 第二章,为菜单创建面板 中,您会找到一个处理 UI 中灯光的食谱。这是在 UI 中玩灯光的好起点。
画布组件
在本章的开头,我们讨论了画布及其主要属性 渲染模式。然而,如果我们查看检查器中的画布,我们可以看到它实际上有三个不同的组件,通常它们一起使用来创建一个画布。为了您的方便,以下截图显示了它们在检查器中的样子:

让我们简要看看它们的职能:
-
画布:这是主要组件,它实际上创建画布,因此 Unity 知道其中的一切都应该作为 UI 渲染。
-
画布缩放器:这个组件控制画布内 UI 元素的整体缩放和像素密度。这种缩放会影响画布上的所有内容,包括字体大小和图像边框。更多详细信息请参阅官方文档:
docs.unity3d.com/Manual/script-CanvasScaler.html。 -
图形射线投射器:这个组件属于事件系统,允许我们根据图形检测鼠标或触摸事件。有关此组件的更多信息,您可以查看官方文档:
docs.unity3d.com/Manual/script-GraphicRaycaster.html。
如果您将要面对这个话题,我建议您研究 Unity 提供的整个事件系统框架(参见下一节)。
画布组
除了我们已经看到的 UI 组件之外,还有一个叫做 Canvas Group 的组件。它允许我们在画布内定义一个组,或者如果你更喜欢,是 UI 元素的子集。这就是在检查器中的样子:

此外,它还提供了一些通用的功能,可以应用于属于该组的所有元素。这些可能包括 alpha,例如,如果您想*滑地使界面的一部分出现或消失,或者如果它是可交互的(或禁用的)。
Canvas Group的另一个常见用途是在 UI 的某些区域不阻止鼠标事件,这意味着玩家可以点击一个位于Canvas Group下的按钮,该Canvas Group的Blocks Raycast属性设置为 false。
事件系统
正如我们在Canvas部分所指出的,每次我们在新场景中创建 Canvas 时,也会创建一个事件系统。实际上,游戏对象包含一系列组件,允许我们在游戏的各个部分之间交换消息。在 UI 的情况下,交换的消息是用户的输入和 UI 本身。没有这个事件系统,交互组件将无法工作。这就是事件系统在检查器中的样子:

如您所见,它被分为模块(这可能会根据您的游戏针对的*台而有所不同;有关更多信息,请参阅第六章,“在糖果雨中穿梭——人工智能导航”)。这里公开的基本功能允许您定义哪些是主要的交互按钮/事件(在独立游戏的情况下)。
然而,就我们的目的而言,我们不需要深入了解事件系统的工作原理,如何更改其设置,或者如何设置自定义消息。为此,我建议您在此处阅读官方文档:docs.unity3d.com/Manual/EventSystem.html。
就我们而言,我们只保留默认设置,每次我们在脚本中使用事件系统时,我们只会使用其基本功能,这些功能将在我们遇到它们时进行解释。
如此一来,我们已经了解了关于 UI 的很多内容,尤其是如果您是一口气阅读这一整节的话。如果是这样,我建议您在继续下一节之前稍作休息,下一节将指导我们如何在游戏中实际使用 UI 界面。
脚本化用户界面
在进入我们的游戏之前,让我们谈谈 Unity 中的通用 UI 编程。我们在上一节中遇到的每个元素都有一个类,该类公开了一些变量和一些我们可以在脚本中使用的功能。然而,所有这些类都在不同的命名空间中。因此,每次我们想要使用这些类时,我们都需要在脚本的开头添加以下代码行:
using UnityEngine.UI;
注意
实际上,我们可以通过每次显式调用命名空间来使用类而不使用using语句。然而,这种方法只有在我们需要很少次使用命名空间时才适用。由于我们正在用 UI 编程,因此添加之前显示的代码行来导入命名空间是一种良好的实践。
设计我们游戏的界面
下一步是开始设计我们游戏的布局。您可以在纸上或电脑上完成这项工作;这取决于您最舒适的方式。
在塔防游戏中,UI 通常提供了与游戏交互的方式。通过 UI,可以建造塔、出售它们或升级它们。此外,UI 还用于可视化统计数据,如金钱和生命值。
这里是为我们的纸杯塔防御游戏设计 UI 的初步草图:

如您所见,已经绘制出了一些关键组件。这些主要围绕玩家的交互,以及他们的对手——熊猫在地图上的移动和被攻击的方式。例如,玩家可以放置塔的地方大致标示出来,玩家可以前进的方向也显示了出来。UI 元素,如健康、分数和塔升级,也包括在内。
当然,在这个阶段,一切都是关于从交互角度和外观上尝试什么感觉更好。通过“感觉更好”,并不是直接指美学,而是 UI 的布局。一个地方有太多 UI 元素可能会让屏幕显得拥挤,尤其是如果它们没有被正确解释的话。现在,这并不是说您不能有很多 UI 元素。在某些情况下,游戏(如MMORPG)可以有大量的 UI 元素,但它们需要以有意义和逻辑的方式放置。在一边放购买塔按钮,在另一边放升级按钮是没有意义的。考虑 UI 最简单的方式是基于常规。如果您首先购买一个塔,然后升级它,那么您需要在 UI 中遵循这个过程。
如您所见,我们已经将所有关键组件放置在屏幕顶部。这样,焦点就在地图上。健康状态用大心脏表示,分数用大字体表示,纸杯塔位于屏幕中央。然而,缺少的是一些关于什么是什么的标签,比如分数和购买塔的选项。在下面的截图中,您可以看到最终版本,其中对这些以及一些 UI 调整已经完成:

如你所见,生命值变成了健康条,这在策略游戏中确实更有意义(而对于*台游戏,可能生命值更适合)。由于我们针对的是西方受众,我们开始将重要元素放在左侧。(对于从右到左阅读的读者,我表示歉意,但出于学习目的,我不得不做出选择。另一方面,如果你正在阅读这本书,这意味着你对英语和从左到右阅读的语言的理解已经足够高,可以证明我们正在开发的这个界面是合理的)。
此外,糖的量是玩家可以用来升级和购买新塔楼的货币。它也是另一个重要的资源,因此被放置在健康条下方。
接下来,你可以找到一个 UI 部分,玩家可以从三个可用的塔楼中选择一个购买。而下面的框则是针对单个塔楼的,当玩家选择一个塔楼时会出现。它允许玩家出售和升级纸杯蛋糕塔。
最后,在右侧是关卡名称,这是玩家会想要考虑的信息,但不如健康和糖那么频繁。
话虽如此,我们为游戏设计了 UI,接下来让我们在下一节开始实现它。
准备 UI 场景
现在我们已经对 UI 系统有了很好的理解,是时候在我们的游戏中实现一些内容来练习一下了。但本章最重要的成果是理解如何在 Unity 框架内编程 UI。
首先,我们需要在我们的场景中创建一个 Canvas,因此,一个事件系统也会被创建。你可以通过在层次面板上右键点击,然后导航到UI | Canvas来实现。从检查器中选择Canvas对象,并根据你的需求调整检查器中的选项。这完全取决于你的目标*台(关于这一点,本书的最后一章有更多介绍),了解哪些选项适合你的需求最好的方式是测试,测试,再测试。
下一步是为我们的界面添加一个漂亮的背景。在我们的包中有一个非常漂亮的蓝色条,你可以在Graphics/UI文件夹中找到它。为了将其放置在我们的界面中,让我们创建一个新的图像。
在层次面板上右键点击,然后导航到UI | Image。我们可以将对象重命名为UI_Background。从项目面板中将ui_blue_top_with_text拖放到图像组件的 Sprite 变量中。由于我们的包已经与我们在第一章中决定的分辨率成比例,Unity 中的*面世界,我们可以直接按下设置原生大小按钮来恢复原始比例。然后,调整条形的大小,并按照以下截图所示放置:

在游戏视图中,它看起来会是这样:

注意
为了您的方便,这个包包含了一个已经放置了标签的 UI 实例。这将使我们节省一些时间。然而,如果您想创建自己的 UI,请记住放置标签并使用正确的字体格式化它们。
因此,我们得到了一个地方来开始开发我们在上一节中设计的 UI。
创建生命条
在我们的游戏世界中,可怕的甜食爱好者熊猫给我们的玩家带来了很多麻烦,尤其是在他们到达渴望已久的蛋糕时。因此,每当它们咬一口,玩家就会失去一些生命。然而,玩家需要一种方式,游戏也需要,来跟踪他的或她的生命。在我们的设计中,我们选择了一个生命条,我们将在本节中实现它。
创建和放置生命条
在我们之前创建的UI_Background中,让我们通过选择UI | Image(您可以直接右键单击UI_Background以将新图像作为其子项)来创建另一个图像。然后,将其重命名为Health_Bar。
将ui_health_bar_frame作为Graphics/UI文件夹中的 Sprite。再次,适当地缩放(如果您愿意,也可以使用设置原生大小按钮,就像我们对条形图所做的那样)并将其放置如下截图所示:

现在,我们需要创建生命条填充。创建一个新的图像并将其命名为Health_Bar_Filling。将ui_health_bar_filling分配给图像 Sprite,并按照以下方式将其放置在场景中:

最后,您应该在层次结构面板中看到以下内容:

现在,在检查器中,我们需要将Health_Bar_Filling的图像类型设置为填充。然后,将填充方法设置为水*和填充原点设置为左,如图所示:

结果,如果我们改变数量变量(您可以在检查器中使用滑块进行操作),条将根据生命条应有的方式或多或少地填充。
现在我们已经准备好通过使用特定的脚本来使生命条实际工作。
编写生命条脚本
最后,在许多关于 UI 的页面之后,我们开始在本章中看到一些代码。再次,正如我们在上一章所说,不要害怕代码,但努力理解为什么它以这种方式工作,这样您将获得编程游戏的能力,在我看来这并不坏。
好的,让我们在脚本文件夹中创建一个新的脚本。如果您愿意,您也可以创建一个名为UI_Scripts的子文件夹,但这取决于您。我们可以将脚本命名为HealthbarScript。
双击脚本以打开它。为了使用 UI 类,我们需要导入命名空间。这可以通过在代码开头添加以下行来完成,正如我们之前所指出的:
using UnityEngine.UI;
现在我们也可以使用 UI 类了,我们需要三个变量。一个是公共变量,允许我们决定玩家可以拥有的最大生命值。另外两个是私有的,用于跟踪附加到Health_Bar_Filling的图像组件,以便更改条形填充,以及玩家拥有的当前生命值:
public int maxHealth; *//The maximum amount of health that the player can possess*
private Image fillingImage; *//The reference to "Health_Bar_Filling" Image component*
private int health; *//The current amount of health of the player*
接下来,我们必须在Start()函数中设置一些变量,特别是使用GetComponentInChildren()函数引用Health_Bar_Filling的 UI Image。我们还需要将当前生命值设置为最大值。这样,玩家将以最大生命值开始,这很有意义。最后,我们调用一个函数来更新生命条的图形,我们将在接下来的几个步骤中实现它:
void Start () {
*//Get the reference to the filling image*
fillingImage = GetComponentInChildren<Image>();
* //set the health to the maximum*
health = maxHealth;
*//Update the graphics of the Health Bar*
updateHealthBar();
}
然后,我们需要公开一个方法来减少玩家拥有的生命值,基于我们传递给它的整数参数。当甜食熊猫咬蛋糕时,或者在最坏的情况下,它会跳进蛋糕里,这个函数将被调用。同时,该函数还应检查生命值是否已达到零。在这种情况下,对于玩家来说,游戏就结束了。因此,这个函数将返回一个布尔值,如果为真,则表示没有更多的蛋糕:甜食熊猫已经吃完了!当然,我们还需要在更改生命值时更新生命条的图形。此外,为了使代码稍微更健壮,当生命值达到零或以下时,当前的生命值将设置为零,而不是负值。因此,我们可以编写以下代码:
*//Function to apply damage to the player*
public bool ApplyDamage(int value) {
* //Apply damage to the player*
health -= value;
*//Check if the player has still health and update the Health Bar*
if(health > 0) {
updateHealthBar();
return false;
}
*//In case the player has no health left, set health to zero and
return true*
health = 0;
updateHealthBar();
return true;
}
最后,我们需要编写一个函数来更新生命值条图形,这是我们之前函数中调用过的那个。首先,基于当前的生命值和最大可用值,该函数计算玩家生命值的百分比(从 0%到 100%),并以浮点数形式在0.0和1.0之间表示。请注意,*1f是将数字转换为浮点数的一种快速方法,因此可以在浮点数之间进行除法,而不是在整数之间。然后,该函数将这个百分比赋值给图像组件的fillingAmount:
*//Function to update the Health Bar Graphic*
void updateHealthBar() {
* //Calculate the percentage (from 0% to 100%) of the current amount of
health of the player*
float percentage = health * 1f / maxHealth;
* //Assign the percentage to the fillingAmount variable of the
"Health_Bar_Filling"*
fillingImage.fillAmount = percentage;
}
保存脚本后,它就准备好了。记住在检查器中分配最大生命值。为了我们游戏的目的,我们将其设置为100。最后,我们的脚本在检查器中将如下所示:

现在玩家有了生命值,我们需要关注存储和显示玩家收集的糖量。
实现糖量计
如果在前一节中我们看到了甜食熊猫如何击败玩家,那么现在是时候给玩家一个阻止它们的方法了。第一步是拥有足够的糖来建造纸杯蛋糕塔。因此,我们需要一个糖量计来跟踪玩家拥有的糖量。
正如我们在设计中看到的,这将是一个数字,而不是像健康那样是一个条形。因此,即使概念相似,实现上略有不同。
创建和放置
创建糖量计的过程与健康条相似,所以让我们开始创建一个新的 Image,将其作为 Canvas 的父级,命名为Sugar_Meter。作为一个 Sprite,你可以使用Graphic/UI文件夹中的ui_sugar_meter文件。将其放置在场景中,并在必要时调整其大小以匹配以下截图:

接下来,创建一个 Text 组件,将其作为Sugar_Meter的父级,并命名为Sugar_Meter_Text。你可以设置你喜欢的字体和颜色。最后,你应该有类似以下的内容:

最后,我们应该在层次结构面板中看到以下内容:

现在,让我们通过脚本让它工作。
糖量计的脚本编写
糖量计的脚本与健康条脚本的工作方式相似。为了使用 UI 类,让我们在脚本开头添加以下行来导入命名空间:
using UnityEngine.UI;
我们需要两个私有变量来存储对Sugar_Meter_Text的引用以及玩家拥有的实际糖量。正如你所看到的,我们没有最大值,因为在理论上玩家可以累积无限的糖量:
private Text sugarMeter; *//Reference to the Text component*
private int sugar; *//Amount of sugar that the player possesses*
在Start()函数中,只需获取 UI 文本的引用并通过一个函数更新图形,这个函数我们将分几步实现:
void Start () {
*//Get the reference to the Sugar_Meter_Text*
sugarMeter = GetComponentInChildren<Text>();
* //Update the Sugar Meter graphic *
updateSugarMeter();
}
现在,我们需要一个通用函数来增加或减少玩家任意数量的糖。我们还需要考虑不可能有负数的糖量。这更多的是为了代码的稳健性,而不是游戏的真实需求。事实上,唯一减少糖量的方式是如果玩家购买了纸杯蛋糕塔(或升级他们的塔),但如果没有足够的糖,他们不会这样做。最后一件要做的事情是更新糖量计的图形:
*//Function to increase or decrease the amount of sugar*
public void ChangeSugar(int value) {
*//Increase (or decrease, if value is negative) the amount of sugar*
sugar += value;
*//Check if the amount of sugur is negative, is so set it to zero
* if(sugar < 0) {
sugar = 0;
}
*//Update the Sugar Meter graphic *
updateSugarMeter();
}
由于我们将在接下来的章节中需要检索玩家拥有的糖量,主要是为了检查他或她是否有足够的糖来购买升级或塔,我们需要一个函数来检索这个值。创建这个函数的原因是糖量是一个私有变量,我们不希望将其公开,因为为了稳健性,我们希望它只通过ChangeSugar()函数来改变,该函数也会更新图形:
*//Function to return the amount of sugur, since it is a private
variable*
public int getSugarAmount() {
return sugar;
}
最后,我们需要一个函数来更新图形。这个函数将玩家拥有的糖量转换为字符串,并将字符串分配给Sugar_Meter_Text的文本组件:
*//Function to update the Sugar Meter graphic *
void updateSugarMeter() {
* //Assign the amount of sugar converted to a string to the text in the
Sugar Meter*
sugarMeter.text = sugar.ToString();
}
一旦脚本保存,我们就不需要在检查器中设置任何参数。因此,我们的糖量计已经准备好测量糖的量。
关于 UI 脚本编写的更多内容——处理程序
我们还需要讨论最后一个话题,因为我们将在第七章中使用这项技术,交易纸杯蛋糕和终极蛋糕之战 – 游戏玩法编程。
假设你想创建一个 UI 组件,当它被点击时执行某些操作。我们可以创建一个带有公共函数的脚本,然后将按钮组件附加到游戏对象上。最后,我们应该在按钮组件上创建一个新的OnClick()事件,以触发我们之前编写的函数。这没问题,但似乎有点繁琐?
另一个例子是,假设你需要拖动一个 UI 组件,因为它是一个浮动窗口。你将如何做?根据我们迄今为止所看到的,这似乎是一个艰巨的任务;但有一个简单的解决方案。
实际上,在我们的脚本中,我们可以直接包含事件系统,通过使用以下代码行:
using UnityEngine.EventSystems;
因此,你将能够通过一些(C#)接口扩展你的脚本。
注意
如果你不了解 C#接口或者不知道如何使用它,你可以查阅任何 C#手册。然而,我推荐观看 Unity 官方文档中的这个视频,因为它直接将接口应用于 Unity:unity3d.com/learn/tutorials/topics/scripting/interfaces。
这些接口允许你在脚本中创建一个函数,每当特定事件发生时,该函数就会被触发。此外,该函数在PointerEventData类中作为参数提供了关于该特定事件的一些信息。例如,要实现我们之前的拖拽行为,我们需要在类声明旁边添加一个处理程序/接口,如下所示:
public class DragTest : MonoBehaviour, IDragHandler {
然后,你需要实现具有特定功能的接口。在这种情况下,我们有:
public void OnDrag(PointerEventData eventData) {
}
小贴士
如果你使用 Visual Studio 作为代码编辑器,你可以右键单击接口的名称,然后从快速操作菜单中选择实现接口来自动创建我们需要实现的函数。
在这个阶段,实现拖拽行为很简单,因为eventData变量中包含了关于事件的全部数据,例如鼠标位置。因此,我们可以编写以下代码行:
transform.position = eventData.position;
注意
或者,你也可以使用Input类,如下所示:transform.position = Input.mousePosition;
要获取事件列表的完整列表,您可以在此处查阅官方文档:docs.unity3d.com/ScriptReference/EventSystems.EventTrigger.html。
关于PointerEventData类及其包含的事件信息的详细信息,这里有一个链接到官方文档:docs.unity3d.com/ScriptReference/EventSystems.PointerEventData.html。
那么,其他所有内容呢?
那么,书中承诺的所有酷炫菜单界面,以及所有用于购买/出售塔的游戏玩法界面呢?嗯,您不觉得我们在本章中学到了很多吗?
事实上,本章提供了大量关于用户界面的信息,我建议您花些时间熟悉所有这些概念,以及完成下一节中的练习。然后,当我们构建第五章中的游戏玩法时,我们将回到实现所有这些内容,“秘密成分是物理学的一点点”。
作业
在本章中,我们首先在第一部分探讨了用户界面的许多方面,而在第二部分,通过在我们的游戏中实现用户界面,我们变得得心应手。然而,在进入下一章之前,我邀请您查看这些练习,以提高您的用户界面设计/编程技能。为了您的方便,它们被分为两部分:第一部分用于提高设计技能,第二部分用于提高编程技能。
提高用户界面设计技能:
-
一个很好的练习:一个很好的练习是找到三款您喜欢的不同类型的游戏,例如策略、冒险和解谜游戏。接下来,对于每一款游戏,写下或绘制并标注每个用户界面元素的功能,以及当用户与之交互时会发生什么。例如,如果用户按下下一个按钮,会发生什么?他们会转到新屏幕,还是会出现一个弹出窗口?您不需要为整个游戏都这样做,但足以了解用户界面是如何工作的,以及图标的位置如何影响用户体验。接下来,尝试实验改变用户界面的位置,甚至用户界面元素的类型。例如,如果左侧有一个生命条,将其移到右侧,或者用文本替换条,看看这会如何改变感觉。这个练习的主要目的是不断尝试不同的用户界面方法。当您屏幕上元素过多,需要删除一些元素时,这特别有用。这样,您就开始发展不同的想法和方法来修改用户界面,以实现不同类型的交互。最后,记录您所做的一切,并为您的成就和学习感到自豪。
-
框架界面(第一部分):使用这本书的包中提供的地图(或你目前正在使用的地图)。现在想象一下,除了顶部有一个用于 UI 的条形栏之外,你还有一个右侧的栏。此外,想象一下有 20 多种蛋糕塔,两种玩家可以收集的糖,棕色和白色,以及玩家可以为每个塔选择两种不同的升级,除了出售之外。在这种情况下,设计一个界面,可以轻松地向玩家展示他们所需的所有内容。特别是要确保界面直观,即使没有解释也可以使用。最后,请你的朋友看看你界面的纸模型原型,并检查它是否真的实现了你的想法。小贴士:不可能一次在界面上放入 20 种塔;因此,你可能想要将它们分成类别,或者有一个可以滚动显示所有塔的区域。
-
框架界面(第二部分):一旦你设计了上一练习的界面,仔细观察我们包中提供的地图颜色(或你目前正在使用的地图)。现在,仔细选择你的 UI 的颜色调色板,以便在不需要太多努力的情况下能够阅读界面,同时它看起来也很愉快,因为它与地图的颜色相协调。
-
框架界面(第三部分):现在是你创建在上一两个练习中创建的设计所需的所有图形的时候了。你可以从头开始创建图形,或者从这本书的图形包中取一部分。一旦创建,将它们导入 Unity 中,包括切片、分割成精灵等。
提高你的 UI 编程技能:
-
一个邪恶的按钮:我们已经看到,按钮上的事件只有在指针释放且仍在按钮内时才会触发。实际上,如果玩家将指针移出组件,事件将不会触发。在邪恶按钮的情况下,一旦玩家点击它,就完成了,动作将被执行。然而,在普通按钮的情况下,动作不是在玩家点击时执行,而是在释放时执行。实现一个邪恶按钮,即使玩家将指针从按钮移开并释放,按钮仍然会触发动作。
-
选中的切换:不幸的是,目前 Unity 没有函数可以检索从切换组中选中的切换。如果你在阅读这本书的时候,Unity 发布了新版本并添加了这个函数,你仍然可以通过完成这个练习并忽略 Unity 的新预置函数来提高你的技能。实际上,能够检索活动切换在许多情况下以及在你的许多游戏中都非常实用。因此,实现一个脚本,该脚本能够根据任意数量的切换的切换组检索并返回选中的切换(如果有;实际上,可能没有)。
-
运行时下拉菜单(第一部分):仔细阅读下拉组件的官方文档,并实现一个可以在运行时更改选项数量的下拉菜单。特别是放置一个切换组,允许玩家选择一组。每组都包含下拉菜单的不同选项。一旦玩家选择了一组,该组的选项就会被加载到下拉菜单中(场景中应该只有一个下拉菜单,你需要动态加载新的选项)。小贴士:由于每组的选项数量是可变的,最简单的方法是清除下拉菜单中所有之前的选项,然后加载新的选项。
-
运行时下拉菜单(第二部分):如果你想更进一步,将之前的练习与输入字段和按钮结合起来,允许玩家向当前选定的组添加选项。
-
运行时下拉菜单(第三部分):如果你对完成之前的练习感到自信,继续通过实现从组中删除选项的可能性来推进。
-
光照指针:如果你已经阅读了 UI 效果组件部分,你会知道存在一个可以创建阴影的特殊组件。在你的场景中间放置一个文本,并应用阴影组件。稍微调整其值,以了解它们是如何工作的,以及如何模拟来自不同方向的光线。现在,实现一个脚本,根据指针位置改变阴影组件,使得指针似乎在文本上有某种光线。因此,如果指针在文本上方,阴影将在下方;如果指针在左侧,阴影将在右侧。顺便说一下,我警告你,完成这个练习的可能后果是创建一些你可能需要花费相当多的时间去玩弄的东西,因为玩弄它可能会变得上瘾!
-
再次预制:我们在上一节中遇到了预制,在游戏开发中它们总是非常有用,尤其是当你有多个场景,我们不想为每个级别再次实现整个 UI 时。由于我们将在最后一章创建不同的级别,因此为每个主要 UI 功能创建预制是一个好主意和好习惯。因此,为生命条创建一个预制,为糖量计创建一个,为 UI 的购买部分创建一个,为 UI 的升级/出售部分创建一个。此外,创建一个包含所有 UI(基本上是画布及其所有子项)的最后一个预制。这样,我们就可以在下一级别的场景中放置这个最后一个预制。然而,请记住,然后在新的场景中手动添加一个事件系统。
-
帧界面(第四部分):如果你至少完成了这个练习的第一部分,你已经设计了一个界面。调整地图的大小,以便你可以创建第一部分中描述的框架。然后,实现这个界面。之后,再次与你的朋友一起测试这个界面,以检查它现在是否感觉数字化。最终,根据朋友的建议进行修改,并且不要忘记分享你的工作,如果你愿意的话,也可以与我分享。如果你还引用了这本书,我会很高兴。
-
负伤害:当我们创建我们的生命条脚本时,我们编写了一个函数来应用伤害,并检查生命值是否达到零。然而,负伤害怎么办?在这种情况下,玩家的生命值会增加,这是不应该发生的。因此,添加一个控制来避免负伤害,例如,如果伤害值低于零,则将其设置为零。
-
治疗玩家:如果你已经完成了之前的练习,那并不是因为我们虐待玩家,而是因为这个功能应该只应用于造成伤害。要治疗玩家,请创建另一个函数,该函数接受一个参数来治疗玩家,并检查生命值是否不超过最大值。如果是这样,它就只将生命值限制在最大值。
-
警告玩家 I:当生命值低于 30%时,意味着甜食熊猫正在吞噬蛋糕,玩家应该收到警告。当生命值低于 30%时,通过在屏幕上仅出现一次的弹出窗口来警告玩家。
-
警告玩家 II:与之前的练习类似,弹出菜单可能会打扰游戏体验。因此,以这种方式警告玩家:当生命值在 20%到 40%之间时,将生命条的颜色变为黄色;当生命值低于 20%时,变为红色。当然,一旦生命值超过 40%,恢复绿色颜色。
摘要
本章从 UI 设计和编程的介绍开始,以便理解 UI 背后的主要复杂性以及为什么它们如此重要。然后,在本章的第一部分,我们详细了解了 Unity UI 系统的工作原理。特别是,我们分析了 Unity 框架的每个组件,并学习了它们的使用和功能。此外,一个部分解释了框架中存在的一些特殊组件,但我们在本书中不需要它们。
你可以在《Unity 5.x 游戏化》这本书中找到更多关于 UI 设计和编程的信息(尽管主要关注游戏化),包括如何使用 Illustrator 创建自己的图形。这本书由劳伦·S·费罗编写,由Packt 出版公司出版。你可以在这里找到这本书:www.packtpub.com/game-development/gamification-unity-5x。
在第二部分,我们开始创建塔防游戏的 UI。特别是,我们实现了健康条和糖量计的逻辑。然而,我们将在第五章 《秘密成分是一点物理学》 中实现更多内容。
在下一章中,我们将不再孤单。事实上,我们终于要见到我们一直期待着的可怕的甜食爱好者熊猫了。
第四章. 不再孤单 - 甜食熊猫出击
"他们生气又饿。小心,甜食熊猫正靠*你的美味蛋糕!
动画和人工智能是赋予游戏中的非玩家角色(NPCs)或复杂物体生命力的核心。前者使 NPC 看起来动态而非静态;后者赋予它们智能,使它们能够在世界中移动和行动。
本章解释了如何使用 Unity 的动画系统,特别是关注 2D 动画。我们将在本书的后面部分介绍人工智能及其在视频游戏中的功能。
本章的第一部分将重点关注 Unity 丰富而复杂的动画系统(有时称为 Mecanim)。在解释每个部分时,我们将逐步将我们的邪恶熊猫栩栩如生。
然而,在章节的第二部分,我们将给邪恶的熊猫移动地图的可能性,触发死亡或吃太多蛋糕以至于爆炸等动作... 字面上的!
尤其是以下内容:
-
动画剪辑及其从精灵图集中创建和处理方法
-
Animator 及其如何为动画构建有限状态机
-
使用 Animator 组件脚本化对象以触发动画并使动画机器工作
-
实现路径点系统以移动甜食熊猫
就像本书的所有其他章节一样,你将在结尾找到作业部分。它包含一系列不同的练习,以提高你的技能并在你的游戏中实现各种不同的功能。所以,让我们准备好学习如何让我们的邪恶熊猫栩栩如生。
准备工作
我们用于本书的图形包包含一个动画精灵图集,用于我们可怕的甜食熊猫。因此,请确保将精灵图集导入为精灵,并将精灵模式设置为多个。实际上,你应该将所有单独的帧放在不同的精灵中。记住重命名它们,这样你以后就不会那么困惑了。如果你使用自己的图形,这也适用。

一只邪恶的熊猫正期待着吃你的蛋糕
动画
生命及其所有生物都是动态的。我们移动,我们的动作,即使是微妙的动作,也表达着情感。如果我们移除这些事物,即使是微小的微笑,生命也会变得乏味和静态。动画效果可以从最简单的事物,如旗帜挥舞,到龙飞翔。只需环顾四周,无论是室内还是室外;总会有东西在移动,或者时不时地移动。即使是石头也会移动,尽管是风吹,或者有人从水面上跳过它们。
Unity 拥有一个复杂的动画系统,也称为 Mecanim,需要时间来习惯。它包括不同的组件。其中一些是专门用于 3D 动画的;其他则可以用于 2D 和 3D。通常,3D 动画比 2D 动画更难,因为它需要调整许多参数,因此需要更多的实践来掌握它。如果你对在 Unity 中学习 3D 动画感兴趣,我建议你阅读关于它的特定书籍。
在这本书中,我们将专注于仅进行 2D 动画。因此,我们的工作流程足够简单,可以在本章中解释。特别是,我们将进行以下工作流程:
-
创建一些名为动画剪辑的文件,以存储我们的动画。我们将从动画 Sprite Sheets 开始做这件事。
-
构建一个有限状态机来控制动画的流程。
-
编写一个脚本以控制有限状态机的触发器。
具体来说,在本节中,我们将看到如何为我们的糟糕甜食爱好者熊猫进行动画制作。在我们的图形包中,我们可以在 Graphic/Enemies 文件夹下找到熊猫的动画 Sprite Sheets。当然,所有的 Sprite Sheets 应该已经切片,正如我们在 第一章,Unity 中的*面世界 中所看到的,如果你使用自己的图形,最好所有单个精灵都有相同的尺寸。但在我们进一步探索 Mecanim 之前,让我们在接下来的两个部分中了解一些关于动画的背景信息。
历史概述
术语 animation 来自拉丁语单词 animates,它是动词 animare 的过去分词;这意味着 赋予生命。这个动词来自单词 anima,意思是 生命,呼吸,起源于希腊语单词 anemos,字面意思是 风,它又源自梵文单词 aniti,意思是 呼吸。直到 1742 年,动词 to animate 才第一次被用来表示 赋予生命 的意思。
现代动画基于运动的概念。因此,值得一提的是,关于运动的第一项研究是在公元前 4 世纪由希腊哲学家 Ζήνων ὁ Ἐλεάτης(在英语中被称为 Zeno of Elea)进行的。我们可以在另一位希腊哲学家 Ἀριστοτέλης(在英语中被称为 Aristotle)的作品中读到许多他的想法。Zeno 以构想许多关于运动的悖论而闻名,探索这个问题以证明运动的不存在。其中一个悖论是箭悖论(也称为 Fletcher's paradox)。我们可以从亚里士多德的《物理学》第四卷中读到一些关于这个悖论的内容,如下所示:
"如果一切在占据相等空间时都是静止的,如果运动中的物体在任何时刻都占据这样的空间,那么飞箭因此就是静止的。"

亚里士多德描述的芝诺的飞矢悖论。你并没有看到箭头向目标移动,而只是看到箭头静止在某个位置上的瞬间,尽管这些位置各不相同。因此,运动是一种错觉。
如前图所示,悖论声称箭头的静止状态,因为箭头在每一瞬间都在空间中的一个非常特定的位置,并且在这一瞬间是静止的。除了悖论的多重哲学含义和解决方案之外,值得注意的是,同样的概念现在也适用于动画。电影和视频游戏(仅举几个例子)中的动画只是一系列静态帧,这些帧以快速连续的方式给出运动错觉。
视频游戏中的动画
动画有助于使我们的创作栩栩如生。它们使生活更加生动,使死者更加令人毛骨悚然!自从像素艺术图形发展到只能用“运动怪异谷”来形容的程度,动画的发展已经走了很长的路。在某些情况下,动画如此逼真,以至于我们一时忘记了我们身处一个不同的现实。
现在,让我们回到游戏动画的起点。太空侵略者、大金刚和暴风雨。如果你曾经玩过这些游戏中的任何一个,那么当你听到“很多内容可以通过非常简单的动画传达”时,你会明白我的意思。无论是太空侵略者横向移动,大金刚跳跃躲避油桶,还是暴风雨中的旋转,都是如此。然而,随着时间的推移,我们变得非常熟悉的动画技术也在不断发展。随着 3D 游戏和随之而来的角色的引入,我们看到动画进入了另一个维度……字面上的。但尽管它们的身体是多边形的,动作是刻板的,比如以下劳拉·克劳馥的图像(尽管现在不那么明显了),动画使我们能够简单地与游戏互动。

取自《古墓丽影》中劳拉·克劳馥的游戏画面
在包含动画背后的一个良好哲学是,将动画视为与观众沟通的一种方式。有时,像爱、兴奋和仇恨这样的情感是传达感受的好方法,就像跑步、跳跃和攻击这样的身体动作是表明玩家当前体验状态的好方法。然而,动画不仅仅包括玩家;树木、动物以及那些不玩游戏或 NPC 也会在游戏空间中互动和移动,有时就像任何受控玩家一样。使用动画与 NPC 的一些最典型的例子是在角色扮演游戏(RPGs)中。
你与之互动的许多角色都会对你产生某种感觉。例如,如果你总是用简短的反应回答他们,他们很可能会表现出轻蔑或震惊的表情,就像下一张图片所示。另一方面,如果你热情友好,并伸出援手,那么他们的表情可能会更加欢迎。

Shepard,显然没有在笑一个笑话(质量效应系列)
时间就是一切,所以当涉及到动画时,确保它们在应该发生的时候发生。按X键跳跃,而跳跃却在 3 秒后才发生,这对你的游戏或玩家来说都不会有好的结果。仅仅为角色或物体制作动画是不够的,你必须将它们组合起来以创造一个沉浸式的环境。如果你想让玩家再次回来,那么沉浸在游戏中是至关重要的。如果你有笨拙的动画导致玩家沮丧,那么玩家可能不会坚持下去,除非故事极其吸引人。即使在大多数游戏中,树木也会进行动画处理,即使只是细微的摇摆或随风摇曳的树叶。这样,它比从地面上伸出的大木桩要好得多。
在考虑动画时,还有另一件事需要考虑,那就是帧率和相应的硬件。你可能有一个精彩的动画序列,比如在《最终幻想 VII:核心危机》中 Sephiroth、Genesis 和 Angeal 之间的史诗级场景(如以下图片所示);另一个例子是在《战地 4》中杀敌时的游戏画面。但如果有延迟,那基本上就没什么了!当为 Unity 创建游戏时,考虑这一点非常重要,尤其是当你针对移动设备时。虽然许多移动设备能够播放一些资源密集型的媒体,但并非所有都能。因此,如果你的最终设备无法跟上,你的辛勤努力很可能会白费,除非当然你找到了另一个目标设备。这在移动设备上的 3D 游戏中尤为常见,你可以在官方 Unity 文档中了解更多信息:docs.unity3d.com/Manual/MecanimPeformanceandOptimization.html。
小贴士
我建议你在完成本章后访问前面的链接,以便更好地了解 Unity 中的动画系统。

Genesis 和 Sephiroth 在《最终幻想 VII:核心危机》中的战斗游戏截图
小贴士
查看这个网站,了解一些在移动游戏中使用动画的绝佳技巧:www.teksmobile.com.au/blog/15-animation-tips-to-make-your-mobile-games-more-engaging。
既然我们已经对视频游戏中的动画有了概述,让我们回到 Unity,了解它如何处理动画,从一般的工作流程开始。
动画工作流程
既然我们已经了解了动画对视频游戏的重要性,让我们更深入地了解一下。Unity 的动画系统基于动画片段的概念;我们将在下一节中更详细地探讨这一点。正如其名所示,它们只是包含单个动画数据的片段(有一些例外)。
动画片段被组织成一个类似于流程图系统的结构,其中不同的节点相互连接(如下一张截图所示)。这个系统被称为动画控制器(Animator Controller),它充当状态机。它跟踪当前应该播放哪个片段,并确定何时动画应该改变或混合在一起。

一个非常简单的动画控制器可能只包含少量片段。例如,一个片段可能是一个物体破碎,另一个是风扇旋转。另一方面,一个更高级的动画控制器可能包含更多的动画,例如与主要角色相关的所有动作,如跑步、行走、闲置、死亡等。此外,动画可以在多个片段之间混合,以看起来更流畅,更不机械,除非当然这是您的意图!
Unity 的动画系统也拥有许多特殊功能,尤其是在使用类人角色时。这些功能允许您从任何来源重新定位类似人类或类人动画,例如动作捕捉、Unity 资产商店,或如 Maya 或 Blender 这样的软件,然后将它们应用到自己的角色模型上。除了应用这些动画外,您还可以调整角色的肌肉定义。这些特殊功能是通过 Unity 的 Avatar 系统实现的,其中类人角色被映射到一个通用的内部格式(我们不会在这里详细讨论,但您可以在章节末尾的选读部分了解更多信息)。
最终,动画片段、动画控制器和 Avatar 通过动画组件在 gameObject 上汇集在一起。该组件引用一个动画控制器(如果需要)以及相应模型的 Avatar。反过来,动画控制器包含它所使用的动画片段的引用。
动画片段和动画组件
Unity 动画系统的核心是动画剪辑。这些组件包含与对象动画相关的信息,例如它们是否需要在动画过程中改变它们的*移(位置)、旋转等。动画剪辑可以是 2D 或 3D,通常在 3D Studio Max、Flash、Maya、Blender 甚至 Photoshop 等程序中创建。除了使用软件,动画还可以手动创建,例如为 3D 角色绑定(给它一个可以移动的骨架)或创建逐帧移动的精灵,其中每个动作都是单独绘制的。还记得那些老迪士尼卡通吗?嗯,他们使用了类似的过程带给我们一些最珍贵的记忆。然而,如果你的游戏需要的东西不是很复杂,比如开关门,你可以在 Unity 中做到这一点。Unity 提供了一个名为动画窗口的工具(本章后面将详细介绍)。
在 2D 的情况下,这些动画剪辑可以包含任意序列的精灵,就像电影中的单帧一样,并且随着时间的推移改变它们,以产生运动的错觉。通常,在 2D 游戏开发中,精灵图用于这些目的(正如我们在第一章中预期的,Unity 中的*面世界)。因此,我们的图形包中也包含这些动画精灵图。

关于动画剪辑的另一种有用的思考方式是想象它们是动作,比如捡起一个物体、行走或跳跃。
注意
在大多数高级情况下,动画剪辑可以包含一个动作的一部分,可以与其他动画剪辑混合或合并。
对于我们的熊猫,我们有以下动画:
-
行走:当我们的熊猫沿着路径移动时
-
死亡:当玩家的杯子蛋糕塔将熊猫击倒时
-
击中:当玩家的杯子蛋糕塔击中熊猫时
-
吃:当熊猫到达关卡末尾并吃掉玩家蛋糕的一块
因此,我们需要创建四个不同的动画剪辑,每个对应这些中的一个。
使用控制器创建动画剪辑
本节中解释的方法是一种快速创建第一个动画剪辑的方法,从精灵图开始,并且作为副作用,会创建一个控制器。然而,我们将在本章的后面处理它。
首先,在场景中创建一个空的游戏对象,并将其重命名为Panda(或者如果你愿意,Sweet-Tooth_Panda)。最终,当你完成对这个对象的工作后,你希望将其存储在一个预制体中。
现在在项目面板中,如果我们选择animation_panda_sprite_sheet并展开它,我们会看到如下内容:

动画的单个精灵/帧应该具有相同的大小。通过在开始时确保这一点,它可以帮助你避免以后出现许多头痛问题。因此,如果精灵图已经做得很好,它应该可以在精灵编辑器中的按单元格计数网格模式下轻松切片,就像我们在第一章中做的那样。在我们的包中,我们的精灵图已经准备好了,每个精灵均匀分布,但如果你使用自己的图形,并且精灵图没有所有帧的大小相同,你可能需要在图形程序(如 Photoshop 或 Gimp)中修改它,以便精灵相应地分布。
这是精灵编辑器中的最终结果:

然而,如果你移动项目面板底部滑块(正如我们在第一章中学习的),你将能够看到所有单个精灵,如下图所示:

选择属于行走熊猫动画的所有精灵,并将它们拖动到我们之前创建的熊猫游戏对象上:

在我们的案例中,行走动画有 11 个精灵,然后是 5 个击打动画的精灵,10 个死亡动画的精灵,最后是 16 个进食动画的精灵
小贴士
有可能最后一张精灵图与第一张相同。根据情况,你可能不希望它因为很可能会在动画中引起中断,比如行走序列中的延迟。在这种情况下,你只需选择除了最后一张之外的所有精灵。
Unity 会询问你将动画剪辑保存到何处以及使用哪个名称。我们可以将其命名为Panda_Walk_Animation并将其保存在我们的Animation文件夹中。如果你没有它,你可以在Asset文件夹下创建它。这样,正如我们在第一章中讨论的,我们可以保持我们的项目整洁有序。
当我们选择Panda对象时,我们可以在检查器中注意到已经添加了两个组件。一个是精灵渲染器,我们已经在之前的章节中讨论过。另一个是动画器组件。让我们在下一节中详细了解它。

注意
如果你导航到动画文件夹,除了我们刚刚创建的动画文件外,你还会找到一个名为Panda(或Sweet-Tooth_Panda,因为名称是从游戏对象中取的)的动画控制器。为了我们的目的,最好将其重命名为更描述性的名称,例如PandaAnimatorController。我们将在本章后面了解更多关于它的内容。
动画组件
Animator 组件的主要功能是持有 Animator Controller 的引用,它定义了我们的动画剪辑应该如何播放。此外,它控制何时以及如何混合和/或在这之间进行过渡。我们将在下一节中探讨控制器。
Animator 组件有一些可以调整的参数。让我们看看主要的几个:
-
控制器:这是 Animator Controller 的引用,是最重要的变量。如果没有设置,Animator 组件将无法工作。在前面的图片中,控制器设置为
Panda(或者如果您已重命名它,则为PandaAnimatorController),这是我们刚刚创建的控制器。 -
Avatar:仅适用于 3D 人形角色参数,您可以忽略它(然而,如果您想了解更多,请查看本章后面的关于动画的更多信息部分)。
-
应用根运动:您也可以忽略这一点(然而,如果您想了解更多,请查看关于动画的更多信息部分)。
-
更新模式:指定 Animator 何时更新以及它应该使用哪个时间尺度。正常模式与更新调用同步更新控制器,Animator 的速度与当前时间尺度匹配。如果时间尺度减慢,动画也会减慢以匹配。Animate Physics模式则相反,它与FixedUpdate调用同步更新 Animator,这些调用由物理引擎使用。这在您所动画化的对象具有物理交互时很有用,例如,如果角色需要推动或拉动一个刚体(关于物理的更多内容将在下一章中介绍)。最后,Unscaled Time模式与更新调用同步更新 Animator,就像正常模式一样,但当前时间尺度被忽略,它总是以 100%的速度播放。例如,当您暂停游戏但仍然想要动画化部分 UI 或暂停菜单本身时,此模式很有用。
-
剔除模式:指定动画的剔除模式(关于这一点将在本章后面的关于动画的更多信息部分中详细介绍)。
此外,在 Animator 组件的底部,有一个包含有关我们使用的 Animator 控制器一些有用信息的信息框。目前,唯一相关的信息是剪辑数量,它告诉您控制器使用了多少个动画剪辑。您可以在关于动画的更多信息部分了解更多关于这个信息框的内容。

创建其他动画剪辑
现在,我们需要分别为熊猫死亡、吃或被击中时创建剩余的动画片段。这次,我们不想生成控制器。我们有两种选择。第一种,我们仍然像之前那样将其他组精灵拖放到熊猫游戏对象上。结果,Unity 仍然会要求为动画片段命名和指定位置,但不会生成另一个控制器。这是最快的方法。然而,还有另一种方法。它涉及到动画窗口,但为了学习,我们将使用第二种方法来创建剩余的动画片段。
要这样做,打开动画窗口(点击窗口菜单栏上的动画或使用快捷键Ctrl + 6)。然后,从层次结构面板中选择你的熊猫。你应该会看到如下内容:

动画窗口允许你在 Unity 中创建动画。它使用关键帧之间的插值技术来计算每帧之间的位置和旋转(以及其他参数)。它还支持录制功能以及曲线编辑器。在 2D 游戏开发中,如果可用动画精灵表(如我们的情况),则不太使用此功能(除非你需要微调动画)。然而,这是一个重要的工具,可以避免需要第三方程序来创建你的动画。此外,它还适用于原型设计动画。不幸的是,我们在这章中没有足够的空间详细讨论动画窗口,但你可以在官方文档中了解更多信息:docs.unity3d.com/Manual/animeditor-UsingAnimationEditor.html。
然而,在我们的情况下,我们只是用它来从我们的精灵开始创建和保存动画片段。正如我们可以从前面的图片中注意到的那样,在上一个部分中已经创建了一个行走动画。要创建一个新的动画片段,点击Panda_Walk_Animation,应该会出现如下下拉菜单:

点击创建新片段...,Unity 会询问你保存新文件的位置和名称。我们可以将其命名为Panda_Die_Animation并在Animations文件夹中保存。结果,动画窗口现在应该已经清空,如图所示:

现在,我们可以选择死亡动画的精灵,并将它们拖放到窗口中。因此,精灵动画被加载到动画文件中,正如我们可以从动画窗口中看到的那样:

我们需要重复这个过程来创建击中和吃动画。最后,你应该在Animations文件夹中有以下文件:

有一点要注意。如果你点击其中一个,在检查器中,你可以看到一些选项,关于动画是否可以循环播放以及如何播放,还有一个信息框,显示动画的一些数据量,例如肌肉数量(但这是针对 3D 动画的,我们不会使用这些信息)。

并非所有的动画都旨在循环播放。实际上,除了走路动画之外,其他所有动画都不应该循环播放。因此,对于它们,只需在检查器中取消勾选Loop Time变量,就像这样:

到目前为止一切顺利。在你继续之前,我建议你在Prefab文件夹中创建一个新的预制件,命名为PandaPrefab,并将你的熊猫拖放到那里。
动画师
想象一下你一天中执行的所有不同类型的动作——从你醒来,到煮咖啡、洗澡、准备上班。每个动作都会有不同的动画。对于角色来说也是如此。在大多数游戏中,角色或其他动画对象(动物、树木等)都有一系列动画。就像我们之前的例子一样,每个动画都会对应游戏过程中的不同时刻。例如,当一个角色只是站立并处于闲置状态时,他们并没有做什么,但他们很可能会继续呼吸。在一些游戏中,如果角色长时间闲置,那么就会触发其他动画序列,比如不耐烦的脚趾敲击或完全出乎意料的事情。在第一人称射击游戏中,游戏环境中的物体可能能够被子弹或力量(比如用力撞墙)影响,因此它们可能会破碎、开裂、打开或甚至关闭。Mecanim 使用类似于流程图的视觉布局系统来表示状态机,并允许你控制并编排你想要在角色或对象上使用的动画剪辑。我们将在稍后详细讨论这一点。
动画师窗口
动画师窗口允许你在 Unity 中创建、查看和修改动画控制器资产。
现在我们有了所有的动画文件,我们需要以有意义的方式将它们嵌入到动画控制器中。如果你双击PandaAnimatorController,动画师窗口就会打开,你应该有一个类似于以下屏幕的界面:

注意
如果你在矩形框内看不到你的熊猫动画,就像前面的图片那样,可能是因为你没有将动画窗口链接到控制器就创建了动画剪辑。这不是问题;你只需要在控制器中选择你想要的动画剪辑,并将它们拖入动画师窗口,它们就会被添加。
动画器窗口分为两个部分。深灰色网格的主区域是布局区域。您可以使用此区域在您的动画控制器中创建、排列和连接状态。
您可以在网格上右键单击以创建一个新的状态节点。使用中间鼠标按钮或Alt /Option,拖动以在视图中*移。点击以选择状态节点,以便在检查器中编辑它们,并点击并拖动状态节点以重新排列状态机的布局,如下所示:

这样,我们的状态机将更加清晰和整洁,以便我们进行操作。动画器窗口的第二部分是左侧面板,可以在参数选项卡和层选项卡之间切换(如果您想了解更多关于层的信息,可以阅读本章的可选部分,更多关于动画部分中的动画器中的层)。参数选项卡允许您创建、查看和编辑动画控制器参数。这些是您定义的变量,它们将作为输入进入状态机。我们很快就会详细了解它们。
此外,打开或关闭眼睛图标(在下一张图片中突出显示以便轻松定位)将显示或隐藏参数和层侧边栏,这为您提供了更多空间来查看和编辑您的状态机。

此外,如果我们启用右上角的上锁图标(如下一张图片所示),我们就能使动画器窗口始终聚焦于当前状态机。如果上锁图标被禁用,点击新的动画器资产或带有动画组件的游戏对象将切换动画器窗口。因此,我们就能展示该对象的州机。锁定窗口的好处是,它允许我们保持动画器窗口不显示相同的州机,无论选择了哪些资产或游戏对象。

最后一个有用的切换(位于上锁图标下方)是自动实时链接,它允许我们在运行时看到机器的实际运行情况。为了您的方便,它在以下图片中突出显示,但当我们需要测试我们正在构建的机器时,我们会更多地讨论它:

动画器状态机
正如我们之前提到的,动画控制器是一个流程图系统;具体来说,它是一种有限状态机。但什么是有限状态机?从维基百科,我们可以读到:
"有限状态机(FSM)或有限状态自动机(FSA,复数:automata),或简单地称为状态机,是一种用于设计计算机程序和时序逻辑电路的计算数学模型。它被构想为一个可以处于有限多个状态之一的抽象机器。机器在任何给定时间只处于一个状态;它在任何给定时间所处的状态称为当前状态。当由触发事件或条件启动时,它可以从一个状态转换到另一个状态;这称为转换。特定的 FSM 由其状态列表、初始状态以及每个转换的触发条件定义。"
在我们的特定情况下,状态将是动画。所以,说我们的动画师是特定的状态意味着具有该动画师控制器的游戏对象正在播放该特定动画。如果现在还不完全清楚,我们将在稍后解释不同的部分。
注意
我们将在本书的后面部分更详细地探讨有限状态机,当我们处理人工智能时。
当我们创建第一个动画剪辑时,Unity 为我们生成了一个动画师控制器。但如果你需要手动创建它,这里是如何做的。从项目面板(可能在一个有意义的文件夹中,例如Animations),右键单击并选择创建 | 动画师控制器。一旦重命名,双击它以在动画师窗口中打开它。与自动生成的控制器不同,它已经包含了我们熊猫动画的所有状态,这里没有这样的状态。实际上,我们需要手动导入它们。在我们的熊猫案例中,我们只需要从项目面板中选择我们之前创建的动画文件,并将它们拖放到动画师窗口的网格中。如果你记得,我们把它们存储在Animations文件夹中。始终记住,你可以通过拖动来移动任何状态。这很有用,因为你可以重新组织状态,以便有一个视觉顺序并提高你工作的可读性。
现在,是时候深入挖掘,看看如何为我们的熊猫实际构建一个动画机。如果你创建了一个新的动画师控制器,请丢弃它,让我们使用之前的一个。
动画师状态
动画使角色栩栩如生,因此动画状态为 Unity 中的动画状态机提供了基础。每个状态都包含一个独立的动画序列(或混合树),例如跑步、行走、攀爬、跳跃等。所有这些动画序列将在角色处于相应状态时被触发并播放。当游戏中的事件触发状态转换,例如玩家在跑步时跳过边缘,角色将保持在新的状态,其动画序列将随后接管。
当你在动画师控制器中选择一个状态时,你将在检查器中看到该状态的属性,如图所示:

这些属性及其功能在此列出:
-
名称:这是在 Animator 中引用状态的方式,也是显示在状态顶部的名称。如果它是从动画自动生成的,默认情况下,它将具有与动画片段相同的名称。实际上,我们的四个熊猫状态与它们的相应动画片段具有相同的名称。
-
标签:识别状态或一组状态的一种方式。当需要从脚本中控制动画机器时很有用。就我们的目的而言,我们可以将其留空。
-
动作:这是分配给此状态的动作片段,例如,我们之前创建的动画片段之一,如
Panda_Walk_Animation或Panda_Die_Animation(实际上,它也可以是一个混合树;请参阅本章后面的可选部分更多关于动画)。 -
速度:动画的默认速度。例如,动画的默认速度可能过慢,比如跑步动画,因此需要提高速度。通过改变速度的值,动画可以播放得更快。
-
乘数:一个乘以速度的数字,以增加或减少速度。旁边有一个参数复选框。这允许我们将这个数字转换为动画参数(请注意,乘数不能有独立的价值,只能链接到浮点参数)。这样,我们可以控制某些动画的速度,而无需触摸速度设置(假设它们都链接到相同的参数)。
-
镜像:是否应该镜像状态,这意味着动画片段应该像镜子中播放,左右互换。这仅适用于 3D 人形动画,因此我们不会处理它。注意,这可以转换为布尔参数。
-
循环偏移:确定动画循环是否应该从不同的点开始,该值表示从动画开始处的偏移量。此外,它还充当乘数,可以设置为参数。
-
足部逆运动学:是否应该尊重此状态的足部逆运动学?这适用于 3D 人形动画,因此我们不会处理它。
-
写入默认值:这决定了 AnimatorStates 是否将默认值写回未由其动作动画化的属性。默认情况下,它设置为 true,但取消选中它意味着状态的非动画属性将保持它们之前的价值。
-
过渡:从这个状态起源的过渡列表。它等同于确定在什么条件下这个状态会变成另一个状态。我们将在接下来的几个部分中详细讨论过渡。
-
添加行为:关于此按钮的描述请参阅本章后面的可选部分更多关于动画。
默认状态,以棕色显示,是机器首次激活时将处于的状态。如果你想,你可以通过在另一个状态上右键单击并从上下文菜单中选择设置为层默认状态来更改状态机的默认状态。

在我们的案例中,默认状态是Panda_Walk_Animation状态,所以请确保它被选为默认状态。此外,要添加新状态,在Animator Controller窗口的空白区域右键单击,并从上下文菜单导航到创建状态 | 空。另一种创建状态的方法,正如我们之前指出的,是将动画拖入Animator Controller窗口,结果,你将创建包含该动画的状态。
就我们熊猫的动画状态而言,我们没有特别的需要或设置去调整。毕竟,我们构建的是一个相对简单的有限状态机,与大型、复杂的 3D 动画机器相比。但是,如果你在任何时候觉得我们拥有的四个动画中有一个太快或太慢,只需选择它并改变其速度。在这种情况下,你可以在我们完成状态机之后进行,以便更好地全面了解熊猫动画,并调整这些值来改进它。例如,我将行走、击打和进食动画的速度减慢到 25%,这意味着速度为 0.25,而对于死亡动画,我使用了 0.2。通常,你通过试错来找到这些值。
注意
注意,你只能将 Mecanim 动画拖入控制器;非 Mecanim 动画将被拒绝。此外,状态不一定只包含单个动画剪辑。实际上,它们也可能包含混合树。你可以在本章后面的可选部分关于动画的更多内容中了解更多关于它们的信息。
特殊状态
动画机也有一些特殊状态,如图中所示:

任何状态是一个始终存在的特殊状态。任何状态意味着它不能是转换的终点。例如,跳转到任何状态不能用作选择随机状态进入下一状态的方法。任何状态存在于你想无论当前处于哪个状态都要进入特定状态的情况。这是向你的机器中添加相同的外部转换的更简单方式。进入和退出是确定动画状态机开始和结束的状态。
注意
有其他特殊状态来处理子机。有关更多信息,请参阅本章后面的关于动画的更多内容部分。
动画参数
如我们之前所介绍,在动画器窗口的左侧,有两个标签页:层,这里我们不会处理(但我提醒你,你可以阅读可选的动画器中的层部分),以及参数,我们将在本节中学习。

动画参数是在动画控制器内定义的变量。这些参数可以从脚本中访问并分配它们的值。因此,脚本可以控制或影响状态机的流程。例如,脚本可以设置一个参数,指示动画应该播放多快,例如跑步或行走;这些可以是相同的动画,只是以不同的速度播放。在更复杂的行为中,相同的参数可以是根据玩家的输入在合适的行走动画和跑步动画之间切换的条件。
要添加一个参数,点击图中突出显示的小+按钮:

注意
然而,如果你想删除一个参数,请在列表中选择该参数,然后按删除键。
出现一个下拉菜单,询问我们想添加哪种类型的参数:

它们可以是四种基本类型:
-
Int:一个整数(整个数) -
Float:一个带有小数部分的数字 -
Bool:一个真或假值(由复选框表示) -
Trigger:一个布尔参数,当被过渡消耗时由控制器重置(由圆形按钮表示)
可以使用Animator类中的函数从脚本中为参数分配值,具体来说,以下函数具有自解释性:SetFloat()、SetInt()、SetBool()、SetTrigger()和ResetTrigger()。
正如我们已经看到的,参数可以链接到过渡条件,甚至可以链接到状态变量(例如速度乘数),然后由脚本控制。以一个例子来说明,假设你的游戏中的女主角正在骑马。脚本可以根据玩家踢马的程度改变一个浮点参数,该参数与奔跑动画的速度乘数相链接。结果,马的动作将根据玩家的输入(如果玩家正在控制你的女主角)实时改变,马会跑得更快。
然而,对于我们的塔防游戏,我们只需要触发器,特别是三个:一个用于熊猫被击中时,另一个用于它到达终点并吃掉蛋糕时,最后一个用于它在被喷溅击中时死亡。我们可以分别将它们命名为HitTrigger、EatTrigger和DieTrigger。最后,你应该看到以下内容:

注意
当然,这并不是实现此系统的唯一方法。请参阅后面的状态机行为部分,了解如何在状态内使用行为,以便在作业部分您可以使用这种新技术进行自我测试,以以不同的方式实现此类行为。
动画师过渡
过渡使我们能够从有限状态机的当前状态切换到另一个状态。当满足某些条件时,它们可以被触发。正如其名称所暗示的,它们处理当前状态如何过渡到目标状态以及这两个状态应该如何合并以实现*滑过渡。
它们表示为两个状态之间的单向箭头。要创建两个状态之间的新过渡,请从过渡应开始的状态(在我们的案例中,Panda_Walk_Animation)右键单击并选择创建过渡,如图所示:

然后单击另一个状态以创建它们之间的过渡。在这个例子中,我们正在从Panda_Walk_Animation过渡到Panda_Hit_Animation,如图所示:

过渡设置
如果您单击箭头,您可以在检查器中看到过渡设置/属性,如下面的截图所示:

如果您愿意,可以通过在下面的字段中输入来为过渡分配一个名称(您需要按Enter键来确认您的选择):

因此,其名称将在包含该过渡的状态中显示(例如,在我们的例子中,在Panda_Walk_Animation状态内):

重命名过渡是您的选择。有人喜欢给出合适的名称,有人则将其引用为初始状态和最终状态,这是默认名称。然而,如果您决定重命名它们,请记住给出有意义的名称;它们是否长无关紧要。
让我们详细说明每个设置,以便更好地理解它们的含义。请记住,其中一些设置涉及特定条件,我们将在不久后探讨。
-
具有退出时间:如果此设置为 true,则过渡只能在退出时间变量指定的时刻发生。
-
退出时间:如果启用了Has Exit Time,此值表示过渡生效的确切时间。这以归一化时间(百分比值)表示;因此,例如,退出时间为 0.65 表示在动画播放了 65%的第一帧时,退出时间条件为真。在下一帧,条件将为假。对于循环动画,具有小于 1 的退出时间的过渡将在每个循环中评估,因此您可以使用此功能在动画的每个循环中根据适当的时机进行过渡。具有大于 1 的退出时间的过渡将只评估一次,因此它们可以用于在固定数量的循环后退出。例如,退出时间为 4.5 的过渡将只评估一次,在四个半循环之后。
-
固定持续时间:如果启用,过渡时间以秒为单位解释;否则,它以 0 到 1 之间的百分比(归一化时间)解释,例如 0.5,它将表示 50%。
-
过渡持续时间:这是过渡的持续时间。这还将确定过渡图中两个蓝色标记之间的长度(参见下一节)。
-
过渡偏移:这是动画(过渡到的目标状态)开始播放的时间偏移。例如,值为 0.4 表示目标状态将在自己的时间线的 40%处开始播放。
-
中断源:这允许您控制允许过渡中断当前过渡的环境。特别是,您可以选择五种不同的模式:
-
无将不允许任何内容中断过渡。
-
当前状态仅允许当前状态内的过渡中断过渡。
-
下一状态允许过渡被其他过渡中断,但仅当这些过渡在目标状态内时。因此,如果目标状态有一个准备触发的过渡,它将中断此过渡并触发。
-
当前状态然后下一状态允许过渡被当前状态或目标状态的过渡中断。然而,如果过渡的条件在当前状态和目标状态上同时为真,则前者将具有优先权。例如,如果有两个过渡准备触发,但一个在当前状态,另一个在目标状态,则第一个过渡将被触发并中断当前正在播放的过渡。
-
下一状态然后当前状态仍然允许过渡被当前状态或目标状态的过渡中断。但与当前状态然后下一状态相比,如果过渡的条件在当前状态和目标状态上同时为真,则后者将具有优先权。
-
-
有序中断:这确定当前转换是否可以被其他转换独立于它们的顺序中断。
小贴士
现在您可以使用转换,请记住经常重新排列您的有限状态机以提高可读性。您应该始终将状态放置得使所有转换都清晰可见,并且理想情况下它们不应交叉太多。
转换图
Unity 还提供了一种通过转换图以可视方式调整这些属性(如上所述)的有用方法,转换图位于上一节设置下方。
可以手动调整转换设置,通过在之前看到的字段中输入数字,或者使用转换图,当操作视觉元素时,它将修改值。

带有清晰时间线的转换图;我们可以以可视方式调整转换将如何发生
在前面的图中,您可以执行以下操作之一:
-
通过拖动出标记来更改转换的持续时间。
-
通过拖动入标记来更改转换的持续时间和退出时间。
-
通过拖动图底部显示的动画片段来调整转换偏移量。
-
通过拖动播放标记预览转换,逐帧导航以调整动画片段的混合方式。预览窗口位于检查器的底部。

这是预览窗口,您可以在这里旋转、缩放和播放动画,以及显示它们的旋转中心点(或 3D 模型的质量中心)并更改预览播放的时间比例。
注意
如果转换涉及混合树作为两个状态之一(或两个状态),混合树参数也将出现在转换图中。您可以在本章后面的关于动画的更多信息部分找到有关混合树的一些更多信息。
转换条件
到目前为止,我们已经看到了许多转换的设置,但它们实际上何时被触发?这就是为什么在设置底部有转换条件。您可以看到它们在这里:

要添加条件,请按+按钮。要删除,选择其中一个并点击-按钮。此外,您可以通过拖动它们的左侧手柄来重新排列它们。然而,顺序不会影响转换背后的逻辑(只是其实施,以及可能的项目可读性)。
这些条件可以与参数进行核对。对于int和float参数,我们可以将它们与一个固定数字进行比较。所以如果参数中的值是大于或小于固定数字。对于int参数,我们还可以检查它们是否与固定数字相等或不相等。布尔值,相反,可以检查它们是真还是假。最后,触发器不能与某物进行比较,但条件检查它们是否被触发。
这里是一个使用所有四种参数的条件的示例:

请注意,只有在该时刻所有条件都得到验证时,过渡才会执行/执行。如果没有条件,则过渡在退出时间指定的时刻触发。
注意
如果有退出时间没有被勾选,并且过渡没有条件,那么 Unity 将忽略该过渡。所以它就像过渡不存在一样。
现在我们已经学会了如何设置过渡,我们将在下一节探索一些有用的功能来测试它们。
测试过渡
如果需要测试过渡,有两个有用的功能需要了解。第一个是独奏,第二个是静音。如果你选择一个过渡,你可以在检查器的顶部看到它们:

然而,我建议你以另一种方式设置独奏和静音。实际上,如果我们选择一个状态,我们可以在检查器中找到该状态的所有具有独奏和静音功能的过渡。因此,我们将有一个方便的视图,因为我们可以一次性查看和设置该状态的所有过渡,如下所示(本图中的所有过渡将在下一节中实现):

当选择静音复选框时,该特定过渡将被完全忽略。而,当选择独奏复选框时,所有其他过渡都将被视为静音。此外,在动画器窗口中,可以看到静音过渡为红色,而独奏过渡为绿色:

独奏和静音过渡的示例
如果你拥有这本书的纸质版(因此没有颜色),从Panda_Walk_Animation到Panda_Hit_Animation的过渡是一个静音过渡,所以箭头是红色的。从Panda_Walk_Animation到Panda_Die_Animation和Panda_Eat_Animation的过渡都是独奏过渡,箭头是绿色的。剩下的一个既不是静音也不是独奏过渡,因此它是白色的。然而,这只是一个例子;请随意以最适合你的方式测试。
此外,从官方文档中我们可以找到关于独奏和静音功能的经验法则:
"基本规则是,如果选中了一个 Solo,那么从该状态到其他状态的过渡将被静音。如果同时选中了 Solo 和 Mute,则 Mute 优先。"
最后,值得注意的是,在我写这句话的时候,有一个已知的问题(始终来自官方文档):
"控制器图目前并不总是反映引擎的内部静音状态。"
熊猫的动画状态机
现在我们对如何使用 Mecanim 系统有了更多了解,我们将做我们开始的事情——为我们的熊猫动画创建一个完整的控制器。这是控制器完成后的样子:

如您所见,有四个过渡,而我们只有一个。但我们仍然需要正确设置所有这些。因此,按照以下方式创建和完成过渡:
-
Panda_Walk_Animation到Panda_Hit_Animation:当熊猫在行走时被洒水击中,熊猫将播放击中动画。因此,让我们添加HitTrigger作为条件,并取消选中 Has Set Time 以在行走循环的任何时刻触发过渡。此外,为了使过渡瞬间发生,让我们将 Transition Duration 设置为零,使其从第一帧开始播放击中动画,并将 Transition Offset 设置为零。 -
Panda_Hit_Animation到Panda_Walk_Animation:当熊猫被击中后,它将再次继续向玩家的蛋糕走去。因此,我们需要在动画结束时立即将熊猫从Panda_Hit_Animation状态恢复过来。所以,让我们将 Has Exit Time 设置为 true,并将 Transition Duration 和 Transition Offset 设置为零,因为我们希望过渡是瞬间的。 -
Panda_Walk_Animation到Panda_Eat_Animation:当熊猫最终到达玩家的蛋糕时,它会吃掉那么多的蛋糕以至于会爆炸!因此,过渡需要通过EatTrigger触发,所以将其添加到条件中,并取消选中 Has Exit Time。此外,由于熊猫的所有过渡都应该是立即的,将 Transition Duration 和 Transition Offset 都设置为零。 -
Panda_Walk_Animation到Panda_Die_Animation:被洒水攻击对我们熊猫来说很困难。如果它不能再坚持,它就会死去,而玩家的蛋糕则完好无损。这是一个由DieTrigger触发的过渡,我们需要将其添加到条件中。同样,由于之前的相同原因,取消选中 Has Exit Time 并将 Transition Duration 和 Transition Offset 都设置为零。 -
Panda_Die_Animation到Exit:一旦熊猫死亡,我们希望将其移除。进入Exit状态(因为我们没有子状态机),实际上会让控制器从进入状态/节点重新开始。然而,我们将在发生之前展示如何销毁熊猫。这个动画进入任何其他状态都无关紧要,但选择退出更有意义,这有助于提高你控制器可读性。再次强调,我们希望转换是瞬时的,因此我们将转换持续时间和转换偏移都设置为零;但我们希望动画完成后立即触发这个转换,这意味着将Has Exit Time设置为 true。 -
Panda_Eat_Animation到Exit:我们之前提到的原因也适用于这个转换。熊猫会吃掉很多蛋糕以至于爆炸,然后熊猫将从场景中移除。检查Has Exit Time,并将转换持续时间和转换偏移设置为零。
测试熊猫的动画状态机
在我们进入下一节之前,我们应该检查我们到目前为止所做的工作是否有效。然而,整个系统只有在完成游戏后才会完成。因此,我们需要找到一个既聪明又快速的方式来测试控制器。
最简单的方法是创建一个新的场景,并将熊猫 Prefab 拖放到其中。然后,构建一个包含三个按钮的 UI 界面。更改它们的文本,以便你将拥有触发死亡动画、触发击打动画和触发进食动画,如图所示:

正如我们在上一章中学到的,按钮有On Click ()事件,这允许我们在按钮被按下时调用一些函数。然而,我们没有机会使用这个功能。实际上,我们将在下一章中更多地处理 UI 事件。
现在,你可以选择所有三个按钮,并点击On Click ()事件右下角的小+按钮。我们可以在以下图像中看到这一点:

出现了一个新的事件,如图所示:

将熊猫从层次结构面板拖动到对象变量中,这样你将得到以下内容:

从下拉菜单中,导航到Animator | 设置触发器(字符串)。这样,我们可以设置 Animator 的触发器。所以最后,你应该有这个:

现在,分别选择每个按钮,并将相应的触发器分配给它们。例如,在触发投掷动画按钮中,你应该写下DieTrigger,如图所示:

注意
使用Set Trigger (字符串)函数并不是最佳选择,因为它涉及到字符串的使用。但出于测试目的,这已经足够好了。在下一节中,当我们构建一个控制动画器的脚本时,我们将看到如何使用散列来引用动画器参数作为数字,并提高效率。
因此,现在每个按钮现在都充当我们熊猫的触发器。因此,我们可以按下播放,最终看到我们的熊猫在行走。然后,通过点击按钮,我们可以触发动画器中的转换并看到熊猫改变状态/动画。结果,我们可以测试转换是否工作良好。请随意调整任何您想要的参数,例如动画的速度或某个转换的转换图,以满足您的需求。
你还记得我们讨论动画器窗口时提到的自动活链切换吗?一旦你进入播放模式,这就是激活它的正确时机。结果,你将能够在你的动画器窗口中看到机器的状态的视觉表示。
例如,在下面的屏幕截图中,执行了行走循环,并且还显示了一个显示动画进度的条形图。这可以帮助你大量调整动画器控制器。

现在,一旦你对你的更改感到满意并且已经应用了它们,如果你想的话,保存场景,然后回到我们的主场景。然后,我们准备好为我们的熊猫创建一个或两个脚本。
动画脚本
最后,我们为我们的熊猫准备好了所有的动画以及一个根据某些触发器改变它们的控制器。然而,到目前为止,没有任何东西会设置动画器的触发器(除了其他场景中的我们的 UI 测试按钮)。因此,我们需要为熊猫创建脚本,这个脚本不仅包括熊猫的行为,还会触发正确的动画。在下一节中,我们将学习如何在脚本中调整动画器的参数。但在我们到达那里之前,让我先向你介绍一个非常强大的工具:状态机行为!
状态机行为
动画机的每个状态可以包含一个或多个行为。这些是扩展StateMachineBehaviour类的脚本,包括以下函数/事件,具有关于它们何时被调用/触发的自解释名称:OnStateEnter()、OnStateExit()、OnStateIK()、OnStateMove()和OnStateUpdate()。
特别是,你需要从母类中重写这些函数,并且它们接受三个参数作为输入。第一个是动画器本身,第二个是AnimatorStateInfo,它存储有关当前状态的信息,最后是一个表示层的整数。在我们的案例中,因为我们不会使用除了基础层以外的任何其他层,所以它总是零。它们具有以下签名(以OnStateEnter()为例):
override public void OnStateEnter(Animator Animator, AnimatorStateInfo stateInfo, int layerIndex)
因此,我们可以在一个状态下控制一切。实际上,状态机行为是一个非常强大的工具。一旦你创建了一个扩展StateMachineBehaviour类的脚本,选择你想要添加该脚本的州。然后,点击检查器底部的添加行为按钮,如下所示:

如果你的类包含变量,它们在检查器中的显示方式与其他脚本相同,并且可以为该特定状态进行配置。以下是一个包含一些变量及其显示方式的状态机行为:

注意
使用静态变量时要小心,因为它们在所有控制器的状态机行为实例之间是共享的!实际上,在这个上下文中不使用静态变量是一个好的实践。
现在,想象一下你的游戏中的英雄角色拥有携带和持有不同武器的能力。在这种情况下,使用机器行为,你可以检查角色手持的是哪一把武器,如果它与传说中的火焰剑相匹配,当我们的英雄挥砍敌人时,就会添加火焰粒子效果。另一个例子可能是,一些角色共享相同的动画机器,因为它们真的很相似,但其中一些角色在跳跃后可以滑行。因此,你可能想要检查这一点,并以某种方式修改你的动画机器的一些参数。
简而言之,唯一的限制是你的想象力(以及计算能力),你可以增强你的动画机器。当然,你可以用状态机行为实现的所有事情都可以用其他方式完成,但它们提供了一个简单且相当直观的方式来做到这一点。熟悉并学习如何使用这个工具并不需要太多时间。
现在我们已经知道了状态机行为是什么,让我们为我们的熊猫创建一个状态机行为!
摧毁行为
当我们的熊猫被可怕的喷水雨或吃太多蛋糕杀死时,我们需要以某种方式让熊猫从场景中消失。然而,我们需要在播放死亡动画之后,以及我们在书中稍后将要看到的更新游戏玩法之后完成这个操作。
正因如此,我们才创建了从死亡和进食动画到退出状态的额外两个转换。这些转换将在相应的动画播放完毕后执行。此外,这些动画是由我们在单独的脚本中触发的(见下一节),所以在熊猫死亡之前,我们有更新游戏玩法的机会,比如玩家的糖分或健康值。
状态机行为允许我们达到这样的控制水*,即在熊猫播放特定动画结束后摧毁熊猫。因此,我们可以创建一个新的脚本,并将其命名为有意义的名称,例如StateMachineBehaviour_DestroyOnExit。现在,双击脚本以打开它。
首先,我们需要扩展StateMachineBehaviour而不是MonoBehaviour。我们可以直接用前者替换后者。由于脚本不再扩展MonoBehaviour,我们也可以移除Start()和Update()函数。最后,我们应该得到以下结果:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StateMachineBehaviour_DestroyOnExit : StateMachineBehaviour {
}
接下来,我们需要覆盖上述状态机行为中的一个函数。特别是我们想要覆盖OnExit()函数。所以每次状态改变到另一个(在死亡和进食状态的情况下意味着动画播放后立即),熊猫将被销毁。我们可以轻松做到这一点,因为函数的一个参数就是 Animator 本身,我们可以从中检索 Animator 附加的 gameObject 并将其销毁。因此,我们只需添加这个函数:
override public void OnStateExit(Animator Animator, AnimatorStateInfo stateInfo, int layerIndex) {
* //Destroy the gameobject where the Animator is attached to*
Destroy(Animator.gameObject);
}
保存脚本,然后选择死亡和进食状态。从那里,点击添加行为并选择StateMachineBehaviour_DestroyOnExit。
一旦完成所有这些,我们就完成了!现在,每次播放死亡或进食动画时,在动画完成后,熊猫将被销毁。下一步是看看如何在控制器中实际触发状态。
熊猫脚本
我们需要创建一个新的脚本,这次让它从MonoBehaviour派生。我们可以称它为PandaScript。然后,我们可以在其中创建一些变量。让我们从一个公共变量开始,用于跟踪熊猫的生命,另一个用于其速度:
*//Public variables that express the characteristic of the Panda*
public float speed; //The movement speed
public float health; //The amount of health
然后,我们需要一个变量来存储 Animator 的引用。因此,当我们需要在 Animator 中触发动画时,我们可以使用这个变量:
*//Private variable to store the Animator for handling animations*
private Animator Animator;
正如我们在Animator 参数部分所学,在 Animator 中设置参数有不同的方法。然而,它们有两种版本:一种是通过 ID 或哈希引用参数,另一种是作为字符串。后者当然是最直观的,但由于它依赖于字符串处理,所以比第一种慢一些。因此,只要可能,最好使用哈希(见信息框)来引用 Animator 中的特定参数。因此,我们可以将这些哈希存储在某个变量中,以便快速使用它们。
注意
在计算机科学中,当我们需要将任意大小的数据映射到固定大小的数据时,我们使用Hash函数。这个函数的结果被称为哈希值、哈希码、摘要或简单地称为哈希。这些Hash函数的主要用途在密码学和数字安全中。每次你进行数字签名时,幕后也有一处使用Hash函数。然而,它们在其他上下文中也被使用,例如在 Unity 中用于优化。
在我们非常具体的案例中,我们有一组参数,它们需要一个整数 ID,这样它们就可以在 Animator 中快速引用。从参数的名称(一个字符串)应用一个Hash函数,由于字符串可以是非常长的任意长度,因此有无限多的组合,将其映射到一个有限的整数集合上,这些整数可以用一个单一的int变量表示(因此最多可达二十亿一千四百七十万四千八百三十个六百四十七)。所以,在您的视频游戏中,每次使用int变量来存储金钱或生命值,就像我们在上一章中所做的那样,2,147,483,647是最大值。通常对于视频游戏来说,这个限制对于您可能想要的任何整数参数来说都足够好了,尽管如果需要的话,可以通过特殊的数据结构来克服它。因此,从 Animator 参数的名称中,我们可以得到一个可以用来引用参数的数字。
这些哈希值是 Animator 无关的,因为它们仅基于参数本身的名称。因此,它们可以通过静态函数Animator.StringToHash()计算或检索,该函数接受参数的名称作为输入,并返回用于 Animator 的数值表示。
在我们具体的案例中,我们有三个触发器,我们可以将它们的哈希值存储在以下变量中:
*//Hash representations of the Triggers of the Animator controller of the Panda*
private int AnimDieTriggerHash = Animator.StringToHash("DieTrigger");
private int AnimHitTriggerHash = Animator.StringToHash("HitTrigger");
private int AnimEatTriggerHash = Animator.StringToHash("EatTrigger");
下一步是在Start()函数中获取 Animator 控制器的引用,以便在其他函数中使用。我们可以通过使用GetComponent()函数来实现这一点,该函数返回指定为Type的组件,该组件附加到与该脚本相同的 gameobject 上。因此,我们可以在Start()函数中简单地添加这一行:
void Start () {
* //Get the reference to the Animator*
Animator = GetComponent<Animator>();
}
现在,关于模块化工作流程,我们可以创建一些私有函数来实现控制状态机的逻辑。然而,我们将等到需要调用它们的时候再处理它们。
因此,现在我们需要一个函数,允许我们的熊猫移动到地图上的一个点。这个函数接受一个Vector3作为输入参数,它是地图上的目标点。根据速度变量,它为熊猫创建一个步骤。然后,使用MoveTowards()函数,它将熊猫移动一步,朝向目标点:
*//Function that based on the speed of the Panda makes it moving towards the destination point, specified as Vector3*
private void MoveTowards(Vector3 destination) {
* //Create a step and then move in towards destination of one step*
float step = speed * Time.deltaTime;
transform.position = Vector3.MoveTowards(transform.position, destination, step);
}
当熊猫被玩家的一个纸杯蛋糕塔的喷雾击中时,将调用另一个函数。它的输入是一个浮点数,表示熊猫从击打中受到的伤害量。因此,该函数从这个值中减去熊猫的健康值,然后检查健康值是否小于零。如果是这样,该函数通过设置DieTrigger参数来触发死亡动画。我们不需要销毁熊猫,因为一旦触发死亡,状态机行为将负责处理它。另一方面,如果熊猫还没有死,该函数将播放击打动画:
*//Function that takes as input the damage that Panda received when hit by a sprinkle.*
*//After have detracted the damage to the amount of health of the Panda checks if the Panda*
*//is still alive, and so play the Hit animation, or if the health goes below zero the Die animation*
private void Hit(float damage) {
*//Subtract the damage to the health of the Panda*
health -= damage;
*//Then it triggers the Die or the Hit animations based if the Panda is still alive *
if(health <= 0) {
Animator.SetTrigger(AnimDieTriggerHash);
}
else {
Animator.SetTrigger(AnimHitTriggerHash);
}
}
我们需要添加的最后一个功能是当熊猫到达其路径的末端并站在玩家蛋糕前面时。在这里,这个功能只是触发吃动画。如何伤害玩家是我们将在下一章中讨论的内容:
*//function that triggers the Eat animation*
private void Eat() {
Animator.SetTrigger(AnimEatTriggerHash);
}
我们可以先保存这个脚本。它在检查器中的样子应该是这样的:

现在不需要担心如何设置速度和健康!当我们讨论游戏玩法编程时,我们会看到这一点。
除非你想阅读下一个可选部分,该部分将引导你了解更高级的主题,否则我们可以这样说,我们已经完成了动画。如果你不想阅读下一个部分,或者想稍后再回来,也许在你完成整本书之后,你可以直接跳到作业和总结部分。否则,休息一下,继续下一部分。
更多关于动画的内容
我们已经了解了 Unity 中动画工作流程的很多内容,但还有很多内容被省略了。本节介绍了 Unity 中动画工作流程的一些相对高级主题,它们对于开发我们的塔防游戏不是必需的。实际上,其中一些只适用于 3D,但我认为提及它们对于有一个关于 Unity 中动画整个工作流程的粗略但完整的了解是有价值的。因此,你可以自由地跳过这一节,或者不用太专注地阅读,以完全理解所写的内容。你总是可以在以后回来,也许在你完成这本书之后,可以更深入地研究其内容。
注意
所有以下章节的目的不是详细解释如何在 Unity 中使用这些工具,而是让你了解它们的存在和功能,以便你在对 Unity 有更多实践经验时再学习它们。
虚拟形象
在 3D 角色,尤其是人类角色的案例中,你需要对你的角色进行绑定,这意味着将 3D 模型的所有骨骼匹配到 Unity 虚拟形象中。如果模型制作得很好并且已经针对 Unity 进行了优化,这个过程可以自动化;否则,需要手动完成,如图所示:

注意
在场景视图中,你可以看到你的 3D 模型,你可以从其中拖放身体部分到虚拟形象中。
以这种方式,Unity 存储了关于 3D 模型的附加信息。它不仅存储了匹配的骨骼,还存储了肌肉。主要原因是通过这种方式,可以执行重定向,这意味着使用相同的动画应用于不同的角色。假设你有一个美丽的行走动画,并且你想将其应用于所有角色。但其中一些角色很高。其他人很胖或者肌肉发达。虚拟形象存储这些附加信息以克服这个问题,并使行走动画适应每个角色。
此外,Unity 还允许遮罩,这意味着丢弃动画剪辑数据的一部分,只使用特定的部分。例如,想象你有一个美丽的行走动画和一个某人喝水动画。假设你想要在角色行走的同时喝水。通过遮罩喝水动画,我们能够将其裁剪到上半身部分。结果,我们可以让行走动画在腿部播放,而喝水动画在角色的上半身播放。
如果你想象一个复杂的游戏,其中角色在行走时可以做很多事情(例如射击、装弹或交谈),这个功能真的非常有用。记住,遮罩可以在不同级别进行。例如,你可以根据不同的身体部分合并超过两个动画,以及子遮罩。可能性是无限的!
正因如此,Unity 在需要时也提供了更详细的映射,例如,对于头部或手部,如图所示,我们可以看到左手部的映射:

所有这些功能与子状态机、状态行为和动画器中的层结合使用时都非常强大。
子状态机
之前,我们看到了 Animator 是一个有限状态机,具有不同的状态,每个状态都是一个动画剪辑。但实际上,并非所有状态都是动画剪辑。其中一些可以是其他东西,例如子状态机。这意味着,一个状态可以包含另一个完整的有限状态机!

子状态机以状态的形式出现在上层,尽管形状略有不同。实际上,转换可以在子状态机上开始或结束,就像任何其他状态一样。
注意
你可以通过名称周围的形状略有所不同来识别子状态机。
在这个场景中,退出状态/节点变得很重要,因为它允许我们完成/退出子状态机并继续到下一个状态。当然,如果满足某些条件,子机器也可以被中断。不言而喻,能够嵌套动画机确实是一个非常强大的工具,可以构建非常复杂的动画器。
层级位置菜单
正如我们所见,状态可以包含子状态和树,这些结构可以重复嵌套。当深入到子状态时,父状态和当前状态的层次结构可以在顶部栏中查看(以下图片中突出显示):

点击父状态允许你跳回到父状态或直接回到状态机的基层。
动画器中的层
现在,如果我们想在 Animator 控制器中创建、查看或编辑层,我们需要确保左侧面板设置为层视图,就像以下图片所示:

这允许你在单个动画控制器中拥有多个动画层。所有这些层都可以同时运行,其中每个层由一个独立的状态机控制。这个过程在例如,你有一个单独的图层在基础层上播放上半身动画时常用,基础层控制角色的整体运动动画(与 Avatar Masks 结合使用)。
要开始,点击加号图标以添加图层。另一方面,要删除图层,选择图层并按Delete键。
混合树
除了动画片段和子状态机之外,动画器中的状态也可以是一个混合树。在动画的不同帧之间应用的一个常见过程是将两个或更多相似的动作混合在一起,这样就会感觉像是一个流畅的动画。例如,行走和跑步动画可能需要根据角色的速度在关键帧之间进行混合。理想情况下,你希望玩家行走时速度较慢,跑步时速度较快。在某些情况下,玩家的跑步速度还可以通过游戏元素(如物品,速度提升)再次增加。另一个典型的例子是在角色转向时向左或向右倾斜,以实现更真实的行为。这可以通过混合树来实现,混合树能够混合动画片段。

一个非常常见的混合树示例;在这里,行走动画被分成三个动画片段,因此角色在行走时可以向左或向右倾斜。
实际上,它们使用线性插值,可以通过一些权重和参数进行控制。Unity 支持混合树的一维和二维插值。

混合树的二维插值。数字和动画片段随机放置,图中的目的是展示上部分的二维插值,其中菱形是不同的动画片段,圆形是控制片段之间混合的二维值。
我们不会进一步探讨混合树,但请记住,它们可以用于实时混合更多动画,以实现极其真实的行为和更*滑的动画。
动画器覆盖控制器
想象一下,你刚刚创建了一个美丽的动画状态机,它充满了层和转换,因为它非常详细。你的角色有状态,允许她施展法术、拿咖啡或挥舞剑。然而,现在,你需要考虑即使是哥布林也能做到这一点。以及你的第二级中的巨魔,以及你游戏后期要与之战斗的精灵。你是否需要为每一个都再次创建一个非常相似的动画机,但只是更改动画剪辑?而且如果你稍后决定稍微改变控制器,你是否需要稍微改变所有这些?感谢上帝,Unity 提供了一个更简单的方法,称为动画覆盖控制器。
你可以通过在项目面板上右键单击并导航到创建 | 动画覆盖控制器来创建一个动画覆盖控制器。它将像其他动画控制器一样成为一个资产。然而,你无法在动画窗口中打开它。如果你在检查器中选择它,你会看到你可以将其链接到一个正常的动画控制器,如图中所示:

只为了学习的目的,我们将我们项目中唯一的控制器,即熊猫控制器拖放到这里。结果,我们使用的所有动画状态都会出现在列表中,我们可以从原始控制器分配不同的动画剪辑。

这样,你不需要复制动画控制器,只需分配新的动画剪辑。一旦动画控制器发生变化,所有相关的动画覆盖控制器也会相应地改变,更新你游戏中使用该控制器的所有角色。不错,不是吗?
动画组件中的剔除模式
现在,想象一下一枚闪亮的硬币的美丽动画。它如此美丽,以至于你决定在你的游戏中包含许多硬币。结果,如果玩家只能看到屏幕上的三个硬币,那么旋转数千枚硬币可能会在计算上变得很重。
考虑另一种情况。玩家刚刚打开了一个开关,触发了一扇非常重且缓慢的门。因此,当门在打开时,玩家会稍微探索一下环境。当他或她回来时,玩家会期望门的动画已经完成,所以门是开着的。这意味着门动画应该在它不可见时也运行,而硬币在任何时候都不需要动画,例如当它在屏幕之外时。
为了优化这个问题,Unity 在动画组件中提供了一个选项,称为剔除模式。这允许我们指定对象何时应该动画化。可能的值如下:
-
始终动画:最昂贵但也是最逼真的解决方案。正如其名所示,对象始终在动画中,所以在这种情况下,当门在屏幕之外时,它也会继续打开。
-
剔除更新变换:这是一个中间解决方案;它仅禁用一些部分,如重定向和 IK,以提高性能,但在需要时仍具有一定的真实感,而不需要付出太多的计算成本。
-
完全剔除:从计算角度来看,这是最便宜的,因为当对象离屏时,它将完全停止动画。
根运动
一些动画,从理论上讲,应该改变角色的位置,例如走路但不喝水。因此,为了在 Unity 中解决这个问题,你可以用脚本移动角色,就像我们在我们的案例中所做的那样,或者使用根运动。正如其名所示,这允许动画本身移动角色的根(或对象或生物)以在空间中获得更真实的运动。
然而,这并不简单,并且有一些缺点。例如,从计算角度来看,它更昂贵,因此,在低端设备上具有根运动的许多角色可能会变得不可行。此外,它需要不同的调整,特别是如果动画做得不是很好。
反向运动学
反向运动学是视频游戏中相对较新的技术。它允许你将目标纳入动画中,有时还能实时计算出适合目标的动画。例如,想象一个角色只想把手放在墙上。反向运动学应该允许你控制动画,将手放在墙上,无论墙是远 1 厘米还是更*。
通常,动画使用所谓的直接运动学(或正向运动学)。基于关节的位置和旋转,可以确定骨骼每个部分的位置。所以想象一下有一个手臂,基于肩部、肘部和手腕的位置和旋转,你可以确定手的位置和旋转。实际上,这些技术来自机器人研究。
反向问题,称为反向运动学,是从手的位置和旋转确定肩部、肘部和手腕的位置和旋转。然而,这个问题不是唯一确定的,因为它可能有无限多的解。因此,解决这个问题并不简单。无论如何,已经开发出不同的技术(在机器人技术中可能涉及雅可比矩阵的逆运算)来解决这个问题。
不同的解决方案的问题在于,其中一些可能会导致非常不寻常的姿态。再次以手臂为例,一个为了将手放在墙上而将肘部抬到眼睛高度的解决方案并不真实(见下一张图片,左侧)。虽然这可能在应用于机器人技术时是一个相对问题,但在动画中这确实是一个大问题,因为我们希望我们的角色是可信的。
因此,已经开发出其他技术来解决这一问题,并对社会行为(针对人形角色)进行了研究,以了解为什么某些姿势比其他姿势更真实。例如,前一张图片中的姿势很累人;没有人会把手放在墙上这样。实际上,这也涉及到物理,因为我们的大脑试图以尽可能少消耗能量的方式控制我们的身体。这导致了一种无意识的行为,我们在其他人身上也能识别出来,我们的游戏角色也需要这种行为。
Unity 实现了其中一些技术,Mecanim 支持人形角色的一些逆运动学,前提是他们有一个正确配置的化身。然而,我们不想深入探讨这个问题。我只是把最好奇的读者留在这里,提供官方文档的链接:docs.unity3d.com/Manual/InverseKinematics.html。
注意
另一个研究并使用逆运动学的例子是用于虚拟现实中的虚拟存在。最*的 Oculus Touch 允许您在 Rift 头显内拥有您的手,但不能是肘部和/或肩膀。在实现逆运动学时——即使考虑到可信度和更节省能量的姿势——软件仍然无法精确地映射您的手臂在空间中的位置,因为您会感到不舒服。然而,在多人游戏中,您可以在虚拟世界中看到其他人,但在现实中却看不到(或像他们那样感受他们的身体),因此可以应用逆运动学。尽管它不会给出游戏中其他玩家肘部的确切位置,但足够接*以令人信服。例如,使用这种机制的其中一个游戏是 Dead and Buried。正如您从下一张图片中可以看到的,您只能看到自己的手,但其他玩家的整个身体您也能看到。

动画组件信息框
如我们之前提到的,动画组件在底部有一个信息框,其中可能包含一些有用的数据。这里再次提供信息框的图片,供您参考:

除了我们已看到的剪辑数量之外,这里简要列出其他信息:
-
位置、旋转和缩放:这些分别表示用于位置、旋转和缩放的曲线总数。动画窗口,我们没有详细看到的一个,允许您创建这样的曲线。
-
肌肉:在动画器中用于人形角色的肌肉数量。
-
通用:动画器用于动画其他属性的数值曲线数量。
-
PPtr:精灵动画曲线的总数;当我们在 2D 中工作时很有用。
-
曲线数量:动画曲线的总数。
-
常量: 作为常量值优化的动画曲线数量。如果您的动画文件包含具有不变值的曲线,Unity 会自动选择此选项。
-
密集: 使用密集方法存储数据(离散值,这些值在线性之间进行插值)优化的动画曲线数量。这种方法比流式方法使用的内存显著更少。
-
流式存储: 使用流式方法存储数据(用于曲线插值的值和时间以及切线数据)的动画曲线数量。这种数据比密集方法占用更多的内存。
传统的动画
在 Mecanim(2012 年,Unity 4.0 版本)引入之前,Unity 使用了一个更简单的动画系统。鉴于从那时起已经过去了一段时间,向后兼容性仍然存在。因此,仍然可以在不更新 Mecanim 或担心软件引发的其他问题的情况下继续处理旧项目。
有些人发现传统的动画系统对于快速原型制作和/或测试动画剪辑很有用,尤其是在对象只有一个动画剪辑时。这是因为它是基于动画组件(下图中所示),并且不应与我们在本章之前看到的 Animator 组件混淆。

您可以在官方文档中了解更多关于传统动画系统信息:docs.unity3d.com/Manual/Animations.html。
因此,除非您有特定的需求,否则您可以完全忽略传统的动画系统,但提到它是值得的,以免您在找到动画组件但不知道它是做什么的时候感到困惑。
在说这个的同时,虽然传统的动画仍然可用,但不建议您使用它来创建新的 Unity 项目。
让我们开始动画吧!!
一个实际例子是思考动画中涉及到的动作。例如,当你走路、打字、喝水、吃饭,任何类型的动作,都尝试用慢动作来做。你开始注意到微妙的动作如何使动画独特并赋予其个性。对于每个动画,比如喝水,你可以用不同的方式来做。拿起杯子,可能用不同的握法,比如用整个手或几个手指握住把手。当你开始更加关注动作时,你应该能够获得一些关于你如何在 Unity 中调整动画的见解,当某些事情感觉不太对劲时。为了再次获得更好的想法,搜索视频,甚至书籍,以探索动画的应用和一些基础。虽然这本书并不是关于动画的指南,但理解基础不仅有助于你提高自己对动画的理解,而且有助于你更好地理解动画师的角色及其所涉及的内容。如果你将来需要与动画师合作,这类信息将对你非常有价值。
作业
在本章中,我们遇到了动画剪辑和动画控制器方面的许多方面。然而,在我们进入下一章之前,我邀请你看看这些练习,以提高你的技能:
-
成为动画设计师:想想你玩过的五款游戏,并选择每个游戏的一部分,比如教程关卡、与 Boss 战斗、穿过森林,甚至是主菜单。现在,列出每个游戏所拥有的动画列表。接下来,移除一些动画,甚至添加一些,并思考这会如何改变体验。它是否改善了体验,或者是否完全改变了氛围?你能通过改变一些动画,让相对愉快的氛围变得非常黑暗,反之亦然吗?通过这样做,你将开始理解动画在不仅为你的游戏带来生命,而且带来情感和设定氛围方面所扮演的重要角色。
-
动画绘制(第一部分):想象一下你需要创建一个动画;你可以在图形程序中简单地完成这个任务。首先,勾勒出手臂的轮廓;你可以用正方形代表手,用矩形代表前臂,用更长的矩形代表上臂。现在,移动每一个部分,使其能够执行摆动动画。理想的情况是设置成我们之前使用的精灵图集那样。然后,将它们导入 Unity 中并测试一下。
-
动画绘制(第二部分):现在,给你的动画添加细节,或者添加其他身体部位,比如腿。此外,你可以自由地添加一些特殊效果,比如熊猫的击打动画。一旦你的动画准备好了,尝试创建第二个和第三个。然后,将它们全部导入,设置好,并构建一个动画控制器来查看它们的动画。此外,你可以润色你的动画,以改善它们之间的过渡。
-
一个不那么无畏的熊猫(第一部分):正如我们所编写的,当熊猫被击中时,它会继续向前移动。然而,熊猫应该有点震惊。只有当动画结束时,熊猫才应该继续向玩家的蛋糕前进。用你想到的任何技术解决这个问题,以便你可以修改熊猫脚本或创建机器状态行为。
-
一个不那么无畏的熊猫(第二部分):如果你已经完成了前面的练习并找到了解决方案,那么是时候让你的代码更加健壮了。在熊猫脚本中添加一个布尔值,并在布尔值设置为 true 时停止熊猫移动,但只有当它被击中时。这样,我们可以在检查器中公开这个变量,为玩家面临的熊猫类型提供更多可能性(在下一章中继续)。
-
状态机行为作为监听器:我们已经以这种方式实现了熊猫脚本,在 Animator 控制器中设置了触发器以改变动画。现在,从熊猫脚本中移除击打和死亡触发器,并实现其他机器状态行为,这些行为检索熊猫的健康值,并在健康值从上次降低或低于零时分别触发击打动画或死亡动画。
-
探索动画窗口:即使我们没有处理动画窗口,你也可以尝试探索它,也许可以通过点击提供的官方文档链接。然后,尝试为我们的糖浆制作一些动画,以便它们在飞向邪恶熊猫的过程中旋转。也许你还可以创建一个碰撞动画。将这些动画包裹在 Animator 控制器中,并在需要时修改 Projectile 脚本。
摘要
因此,我们为我们的熊猫制作了四个不同的动画片段,每个片段对应其可能的行为:行走、死亡、击打和进食。然后,我们将它们包裹在一个控制器中,并构建了一个有限状态机来定义这些动画片段如何通过转换相互链接。最后,我们编写了一个脚本来触发机器的不同状态。
现在我们已经看到了如何为我们的熊猫动画,是时候进入下一章了,也许在喝杯咖啡之后。
第五章。秘密成分是一点点物理
“糖果可能看起来很柔软,但它们是对甜食熊猫的绝佳锐利武器,它们试图偷走你美味的蛋糕!”
本章解释了 Unity 如何处理 2D 物理,并为每个组件提供了描述,以及一些有用的使用示例。虽然我们只为我们的游戏使用 Unity 物理引擎的一小部分,但在本章中,你可以找到一些物理的基础知识,以更好地面对所涵盖的主题,以及 Unity 中 2D 物理的全面洞察。
在本章的第一部分,我们将掌握一些关于物理的基本概念,以便更好地面对即将到来的主题。我们将学习质量、力和扭矩,仅足够理解 Unity 中的物理。
然后,本章的大部分内容将逐步解释 Unity 的 2D 物理引擎及其所有组件和功能。将提供许多示例,以方便学习。
最后,在本章的最后部分,我们将运用所学的概念并将它们应用到我们的游戏中。实际上,我们将通过启用和处理两个物体之间的碰撞,将简单的糖果变成对抗甜食熊猫的可怕武器。
因此,我们将学习以下这些主题:
-
物理学的基本概念
-
理解 Unity 的物理引擎
-
物理设置,这是整个项目/游戏的一般属性
-
不同身体类型和它们的使用方式的刚体组件
-
碰撞体及其使用方法
-
用于对刚体施加约束的关节
-
在游戏世界的特定区域改变物理属性的效果器
-
用于确定碰撞体摩擦和弹性的物理材料
-
在我们的游戏中使用物理组件
就像本书中的其他所有章节一样,你将在结尾找到“作业”部分。它包含了一系列不同的练习,帮助你提高技能并将各种不同的功能实现到你的游戏中。
准备工作
本章学习关于物理引擎的内容不需要任何特定的先决条件。然而,在本章的结尾,我们将继续我们在第四章中留下的塔防游戏,“不再孤单——甜食熊猫出击”。因此,如果你想继续开发“塔防”游戏,你需要完成前几章中的所有其他步骤。
对于那些刚开始学习物理的新手来说,需要大量的耐心,但一旦你掌握了这些概念,你自己的游戏将打开一个全新的可能性世界。所以不要放弃!
游戏中的物理
从不同速度和轨迹飞行的子弹到重力在让你保持在地面上和让你漂浮之间发挥作用,视频游戏中的物理学在使体验更加真实和绝对有趣(大部分情况下)中扮演着至关重要的角色。想象一下,如果每种武器发射的子弹都以相同的方式发射,无论火力如何,那么狙击步枪和左轮手枪之间的区别又在哪里呢?这仅仅是物理学在游戏中扮演角色的一个方面。游戏中的物理学不仅仅围绕轨迹和力;它还可以包括重力、时间旅行和流体动力学。
一些使用物理学的优秀游戏例子包括 World of Goo,Portal 1 和 2,Mario Galaxy,以及 Kerbal Space Program。

图片来自 Portal 2 - 一种减少摩擦的橙色液体,可以给你提供很大的助推
当然,在光谱的另一端,游戏并不总是以最准确的方式复制物理学,而是夸张它或完全忽略它,以允许创新和独特的游戏玩法。一个例子就是在 Assassin's Creed 中从很高的地方跳下,跳进方便放置的干草堆(甚至水)。有些人甚至研究了为了生存这样的跳跃你需要多少干草,你可以在这里找到:www.kotaku.com.au/2009/06/kotaku-bureau-of-weights-measures-studies-fallout-physics-also-beer/
当涉及到在你的游戏中添加物理学时,你必须问自己你希望你的游戏体验有多真实。如果你想你的游戏与现实 100%相同,那么你也必须考虑像死亡这样的事情。如果一个角色可以被子弹击中,他们会出血吗?或者他们可以自己治愈?在某些情况下,如果你的游戏互动过于真实,游戏本身可能最终更像是一个模拟而不是游戏。一个例子是永久死亡(玩家在死后必须从游戏开始重新开始),你可能或可能不在你的游戏/模拟中想要这个。在将物理学添加到游戏中时,所有这些因素都需要考虑,因为原因和结果都会对游戏的可信度和最终的可玩性产生影响。
物理学 – 基础
在本节中,我们将学习一些基本的物理学概念,以便更好地理解 Unity 的物理引擎,并总体上成为更好的游戏开发者。
首先,什么是物理学?亚里士多德(与上一章中提到的那个人同一个人)写了一篇题为 ta physika 的论文,字面意思是自然事物。从这篇论文(尽管在亚里士多德之前许多人已经写过关于自然现象的文章),物理学成为了一门科学。如今,物理学通过数学模型研究物质,它在时间和空间中的运动。最终,物理学的目标是描述整个宇宙是如何运作的。物理学分为四个主要分支:
-
经典力学:这涉及物体的运动
-
热力学:这涉及物体的温度
-
电磁学:这涉及电磁波/粒子
-
量子力学:这涉及亚原子粒子的研究
你不需要将这些看作是独立的实体,而只是同一枚硬币的不同面,这是物理学几个世纪以来试图揭示的世界模型。因此,许多主题跨越了所有这些分支,因此这种划分并不是绝对的。但是,这对于物理学的新手来说是一个很好的概述。
在本章中,我们将只涉及经典力学,特别是刚体的运动。然而,物理学的其他分支和子分支在游戏开发中也非常有用。例如,我们可以使用在着色器中实现的灯光方程,或者流体动力学来模拟海洋和波浪。即使邪恶的巫师施放他最可怕的火球,我们也需要计算许多不同的物理方程,以实现逼真的行为和外观。想象一下只是模拟球体周围的火焰。
大多数情况下,如果你已经有一个像 Unity(或 Unreal)这样的游戏引擎,你不需要深入了解细节。实际上,他们的程序员会为你处理这些。然而,有时你可能会有特殊的需求,需要编写自己的着色器或编写一些基于物理的行为代码。因此,对物理学的了解非常有帮助,并且极其重要。
当然,这并不是一门物理学课程,也不是一本专门介绍物理学编程(例如,可以模拟更多或更少、更高效地模拟某些物理行为的算法)的书。然而,在本章中,我们将巩固物理学的基本概念,以便我们能够大致了解它们是如何共同工作的。因此,本章和接下来的部分将致力于建立对物理学的坚实基础,以便我们更好地理解 Unity 背后的物理引擎。
注意
我知道你们中的一些人可能会觉得物理有点无聊。如果它唤醒了高中的一些不良记忆(或记忆),可能会是这样。但我的个人观点是,了解物理对于成为一名优秀的游戏开发者来说非常重要,尤其是在你需要编写现实行为时。有了这个前提,我保证在本节中,我会尽可能地清晰,并使用非正式的语言而不是严格的数学语言,以便任何人都能理解这些概念。
刚体经典力学可以分为两个子分支:
-
运动学:它涉及对运动的研究。
-
动力学:它涉及运动的原因。
当然,再次强调,它们是同一枚硬币的两面。实际上,动态方程需要运动学方程,以便描述和预测刚体的运动。我们很快就会更详细地了解它们,但首先让我们关注一些基本概念。
世界坐标系和局部坐标系
正如我们在第二章中已经看到的[制作纸杯蛋糕塔],使用世界坐标系和局部坐标系之间存在差异。这也是物理学中的一个重要概念:你的参考系(或坐标系)是哪一个?这个问题是确定不同物理量值的关键。因此,每次你有一个物理量时,你都需要问自己你正在哪个参考系中工作。
在任何情况下,没有一个坐标系相对于另一个坐标系具有特权(尽管有些物理系统在某些坐标系中比在其他坐标系中更容易描述)。因此,按照惯例,当我们开始描述一个物理系统时,就定义了一个世界坐标系。例如,如果你在开车,你的速度是多少?嗯,答案取决于我描述汽车运动时所使用的坐标系或参考系。在汽车本身中,汽车的速度是零,而是世界正在向汽车靠*。而对于行人来说,汽车在移动,而世界没有移动。月球上的宇航员会看到行人以非常快的速度移动,包括整个地球,以及汽车,以不同的速度。所有这些描述都是物理上正确的。因此,物理学中的所有量都依赖于参考系。
在 Unity 和/或 Unreal 等游戏引擎中,始终有一个定义好的世界坐标系(当你有以世界坐标系中心为零的位置时),如下面的图所示:

上述图表显示了如何用线性偏移和角偏移来描述相对于世界坐标系的其它坐标系。此外,你还可以用另一个坐标系来描述一个坐标系,而这个坐标系不是世界坐标系(在上述图表中,看看框架#3的角偏移)。在 Unity 中,你可以将世界坐标系视为全局坐标,而不同的坐标系视为游戏中游戏对象的局部坐标。游戏对象的孩子将以父框架的形式表示。
速度
物理学中的速度描述了一个物体空间和时间之间的关系;换句话说,物体如何随时间变化相对于参考系的位置。当物体不加速时,速度被定义为空间内覆盖的距离除以时间。
注意
对于那些好奇的人,当一个物体在每个瞬间改变其速度,并且我们需要获取特定瞬间的速度时,可以通过使用导数来获取它(这是一个微积分的话题,我不会在这本书中深入探讨)。
速度是一个矢量,因此它在 2D 世界中有两个坐标(在 3D 世界中是三个坐标),表示方向和强度。速度的强度也称为速度。所以不要混淆这两个概念。实际上,速度是一个标量,只是一个数字,如果我们处理的是 2D 或 3D 游戏,它独立起作用;相反,速度会变化(从两个数字变为三个数字,分别表示 2D 或 3D 矢量)。
无论何时我们使用速度这个术语,我们指的是定义好的世界坐标系中的速度(或坐标)。当我们使用相对速度这个术语时,我们是指相对于另一个参考系的速度。例如,想象有两辆汽车朝相反方向行驶,速度都是 50 公里/小时(在世界坐标系中,因为当你看你的转速表时,按照惯例,这是相对于地球的速度)。对于两位驾驶员中的任何一位,他驾驶的汽车没有移动(实际上,当你开车时,你的方向盘并不是以 50 公里/小时的速度远离你的手,但它始终与你保持相同的距离)。然而,他会看到另一辆汽车和另一位驾驶员以 100 公里/小时的速度向他驶来。
质量
在经典力学中,每个刚体都有一个质量,你可以想象成构成该物体的物质的数量。因此,它被认为是物理体的一个属性。在口语中,质量经常与重量混淆。实际上,质量在国际单位制中以千克(kg)为单位测量,而重量在国际单位制中以牛顿(N)为单位测量。因此,秤不是测量你的重量,而是你的质量。重量是一种力,取决于你所在的位置。你在月球上的重量不同,但你的质量是相同的。
有时候,质量也被定义为物体对加速度(当施加力时其运动状态的变化)的抵抗程度的度量。实际上,你需要施加更多的力才能移动一辆卡车,而不是你的笔记本电脑。
然而,值得注意的是,例如,笔记本电脑和卡车(在没有其他外部力的情况下)都受到地球引力的吸引,并且它们都会同时落地接触地面。实际上,当应用牛顿的万有引力方程时,作用在两个物体上的力是不同的,但它们的重力加速度是相同的(因为质量在方程中被简化了)。
注意
对于你们中更好奇的人,这个发现是由著名的意大利天文学家、物理学家、工程师、哲学家和数学家伽利略·伽利莱(Galileo Galilei)在牛顿正式化引力之前很久就发现的。有一个轶事说伽利略曾从比萨斜塔上扔下不同质量但材质相同的球体,以证明它们的下落时间与质量无关。通过这种方式,他证明了亚里士多德的观点——重的物体比轻的物体下落得快,与重量(当时指的是质量)成正比——是错误的,如下面的图所示:

质心
假设你需要计算或预测箭矢的轨迹。考虑到箭矢的整体形状,并知道在每一个点都有不同的力作用,最终会导致一个非常复杂的计算(尽管是可能的)。因此,质心是一个对刚体来说非常有效的*似。它包括将一个物体的全部质量集中到一个点上(这是一个抽象,想象这个物体的所有质量都集中在这个点上)。这个点的位置取决于物体的形状(因为它在物体组成的所有点中是加权的),并且这个点提供了足够的信息来进行非常精确的计算。特别是在实时应用中,这一点变得至关重要,因为运动计算大大简化了。实际上,Unity 只会使用质心来计算。

质心是系统中所有点的重量*均值,其中权重是不同的质量。从图中我们可以看出,质心更靠* m1 和 m3,因为它们更大(质量更大)于 m2。
运动学
术语“运动学”首次在 1840 年左右被使用,作为法语术语cinématique的翻译,该术语由安德烈-玛丽·安培(著名的法国物理学家,经典电磁学的创始人之一——如此著名,以至于国际单位制中的电流单位安培是以他的名字命名的)。然而,这个术语的起源来自希腊语单词kinesis,意为运动或运动。
为了更好地理解物理学中的运动学是什么,让我们参考维基百科:
"运动学是经典力学的一个分支,它描述了点(或粒子)、物体(对象)和物体系统的运动,而不考虑这些物体的质量或可能引起运动的力。作为研究领域的运动学通常被称为运动几何。"
运动学只考虑物体的运动,而不考虑运动的成因。这在游戏开发中带来了一些优势。首先,从计算的角度来看,只考虑物体的运动学要便宜得多。然后,我们可能不想在我们的场景中有一个完全逼真的物体,因此我们可以只定义其运动学属性。我们将在后面详细讨论在 Unity 中处理运动学物体时看到这一点。
注意
当然,在我们的世界中,一个纯粹的动力学物体是不存在的;它们都是动态的。然而,仅仅研究一个物体的运动学,就能让我们对其运动有所了解。
动力学
术语“动态”在 19 世纪被用来表示与产生运动的力量相关,与静态相对。再次强调,它起源于法语术语dynamique,尽管它是由著名的德国数学家和哲学家戈特弗里德·威廉·莱布尼茨引入的。然而,这个术语的起源再次来自希腊语单词dynamikos,意为强大的,以及来自dynamis(力量)和dynasthai(能够,拥有力量,足够强大)。
如果没有明确指出,每次我们处理物理学中的动态事物时,我们都在处理力(它们具有移动物体的某种能力)。从维基百科中我们可以读到:
"动力学是应用数学的一个分支(特别是经典力学),它研究力和扭矩及其对运动的影响,与运动学相对,运动学研究物体运动而不考虑其成因。艾萨克·牛顿定义了支配物理中动力学的根本物理定律,特别是他的运动第二定律。"
因此,动力学建立在运动学之上,并给出了对物体如何运动的更好描述/预测。实际上,通过考虑力、质量、重力、阻力等许多其他因素,它们变得非常重要和相关。
力和扭矩
**"力是那些允许运动的事物;它们负责产生运动。"
每个力都会对物体施加一个加速度,这是用牛顿第二定律计算的。它如此简单而优雅,以至于很容易理解。我们可以这样看待这条定律:

其中 F 代表力,它是一个矢量(因此公式上方的箭头),m 是质量,a 是加速度,它也是一个矢量(因此公式中其符号上方的箭头)。
这意味着如果你对一个质量为 m 的物体施加力,你知道加速度是 F/m。所以,只需将力除以物体的质量,你就可以计算出它将加速多少。请注意,如果我们对两个物体施加相同的力,但它们的质量不同,质量较大的物体将具有较低的加速度,因为力被除以一个更大的数字。
力的概念很重要,因为它是运动的原因,许多物理引擎(包括 Unity 的引擎)允许你指定力。
另一个重要的概念是扭矩,也称为力矩或简称矩,它在某种程度上表示一种使物体沿轴旋转而不是移动的力。如果你把力想象成拉或推,你可以把扭矩想象成扭转。
注意
对于那些对向量叉积有所了解的人来说,扭矩被定义为力向量和力作用点的距离向量之间的叉积。因此,它倾向于产生旋转运动。因此,公式是:

其中 τ(读作 tau)是扭矩,F 是力向量,r 是距离向量,也称为偏移量。
在国际单位制中,扭矩以 N·m(牛顿米)为单位测量。
扭矩的公式稍微复杂一些,因为它涉及到叉积,对于那些刚开始在视频游戏中处理物理并使用 Unity 这样的图形引擎的人来说,不需要立即了解/理解这个公式。然而,重要的是要理解这个量与旋转有关,对刚体施加扭矩意味着它将根据扭矩施加的位置沿轴旋转。只需记住,扭矩不是力,尽管由于我们在 Unity 中处理它的方式,它可能看起来像力。
碰撞
在经典力学中,刚体之间可能发生不同类型的碰撞。它们被分为两类:
-
弹性碰撞,其中所有动能都得到保存。
-
非弹性碰撞,其中部分动能转化为其他形式的能量。
要通过这一章,并不需要很好地理解它们,但重要的是要理解碰撞的类型可能会改变碰撞后的行为。想象两个球:一个向第二个球滚动,而第二个球是静止的。在某个时刻,第一个球会与第二个球相撞。根据发生的是哪种碰撞,结果可能会有所不同。在一个场景中,两个球会向同一方向滚动,它们就像被连接在一起。在另一个场景中,第一个球停止,第二个球开始滚动。在第三个场景中,它们在碰撞后以不同的速度和方向滚动。

三种可能的碰撞后场景
如果你想了解更多,有一个网页以相当简单的方式解释了不同碰撞的基本概念,hyperphysics.phy-astr.gsu.edu/hbase/elacol.html。然而,它是为那些已经清楚了解物理学的基本概念和公式的读者准备的。
备注
本身,hyperphysics.phy-astr.gsu.edu 网站是一个快速参考物理学主要概念的良好来源。然而,要完全理解其内容,需要一定的先决条件。最好的开始方式始终是阅读一本物理书,这可能很无聊,但掌握这些概念是一个巨大的优势。
刚体
正如其名所示,刚体是一个整体*移和旋转的物体。例如,圣诞球、电视或飞盘都是刚体,因为它们的各个部分一起移动。笔记本电脑不是,因为其盖子可以旋转,如果你把框架也视为整体的一部分,门也是如此。但是,这类物体可以用两个不同的刚体来描述,例如门框和门本身,它们可能相互约束(在这种情况下是铰链)。水、空气、牙膏或连衣裙不被视为刚体,因为它们的各个部分不会一起移动。
你游戏中的大多数物体将是刚体(我这样写是因为这是 Unity 将引用它们的方式),或者它们可以被*似为刚体,因此了解它们对于学习 Unity 中的物理至关重要。实际上,Unity 的 2D 物理引擎的大部分内容都集中在刚体上(我们很快就会看到),因此本章就是关于它们的。
模拟衣服、液体和其他非刚体物理实体通常更困难,尽管是可能的。但它们通常用于 3D 游戏,我们不会在本章中处理这些方面。
备注
对于最好奇的你们,这里有一个更精确的定义:刚体是对固体物体的理想化,其中忽略了变形。因此,刚体内部任意两点之间的距离在时间上保持恒定,独立于将作用于刚体的力。
当然,在我们的世界中,这样的物体是不存在的。实际上,它们是理想化模型,但它们很好地*似了固体物体的行为(除非物体的速度接*光速)。
摩擦——线性阻力和角阻力
如果一个孩子问我,“什么是摩擦?”我会回答,“摩擦是那种没有它所有东西都不会停止运动的东西”。即使这不是一个正式的定义,但它有助于理解摩擦是什么。当你考虑物体的动力学时,它就会发挥作用,因为摩擦会产生一个与物理体运动相反的力。有许多种摩擦,它们取决于不同的因素。例如,空气阻力取决于物体的速度,而干摩擦取决于法向力(这取决于表面、物体的位置、其重量,因此也与其质量有关)。
在游戏中,除非有特定需求,否则你不需要所有这些类型的摩擦和阻力。例如,在 Unity 中,你可以控制线性阻力和角阻力。第一个与物体沿轨迹(或者更好,物体的质心轨迹;否则在实时中做会很复杂)的运动相反,而角阻力与物体的旋转相反。因此,在 Unity 中,一个具有非零线性阻力和角阻力的物体最终会停止运动和旋转(在没有其他力的作用下)。
Unity 物理引擎
Unity 集成了物理引擎,能够处理刚体动力学和动力学类型的刚体以及其他物理实体,如衣物。它分为两部分:2D 物理和 3D 物理。重要的是要理解,尽管它们可以在同一场景中共存,但它们是两个独立的实体;它们之间不能相互通信。2D 物理下的物理对象不会与 3D 物理下的物理对象相互作用。
在本节中,我们将探讨 Unity 中大部分的 2D 物理引擎。虽然我们不会使用这里展示的所有组件,但掌握它们对于成为一名更好的 Unity 开发者来说非常重要。
理解 Unity 中的物理
在第二章“制作纸杯蛋糕塔”中,我们学习了脚本排序及其执行方式。我们可能会问的第一个问题是,当渲染帧时物理引擎何时发生?答案是并不直接,因为物理引擎在帧渲染期间会运行多次。无论如何,除了特定情况(例如应用已暂停或在该帧中某些游戏对象被启用/禁用)之外,物理引擎在初始化(因此所有Awake()、Start()和OnEnable()函数)之后运行,并在从玩家那里收集输入和更新游戏逻辑之前。
因此,当我们进行物理计算(我们将在本章后面看到这些)时,我们需要使用FixedUpdate()函数,该函数在每个帧中调用多次,并且所有调用都在物理引擎执行计算之前。因此,如果我们需要获取时间,我们不能使用Time.deltaTime;相反,我们需要使用Time.fixedDeltaTime。
我们可能还会问的一个问题是何时应该使用 Unity 的物理引擎。从理论上讲,您不需要使用它,因为您可以从头开始编写所有碰撞和/或物理行为。在实践中,它以最少的编码解决了许多常见问题,确实很有用。这些问题不一定是物理问题。实际上,其中一些包括某些游戏机制,当使用物理方法时可能会提高您游戏的质量(例如角色的移动)。其他包括从环境中收集信息以编写其他行为(例如需要知道区域内有多少目标的脚本)。
Unity 中的物理设置
首先,Unity 的物理引擎有一些通用设置,应该予以考虑。它们定义了您游戏的全局物理属性,例如重力值、什么会与什么相撞,以及物理模拟应该有多精确。
您可以通过在 Unity 中导航到编辑 | 项目设置 | 物理设置 2D来访问物理设置,如下面的截图所示:

注意
同样,您可以通过在 Unity 中导航到编辑 | 项目设置 | 物理来访问 3D 物理设置。
以下屏幕将出现在检查器视图中(最重要的参数被突出显示,我们将在本节中详细说明):

如您所见,有许多可调整的选项。大多数情况下,特别是对于简单的游戏(如我们正在构建的游戏),默认设置将足够好。但 Unity 为您提供机会根据您的游戏调整它们。
其中大部分涉及 Unity 物理引擎的精确度。因此,在精确度和效率之间有一个权衡,因为更精确意味着从计算角度来看成本更高。
让我们详细说明主要的内容(如果您对其他内容也感兴趣,请参阅本章后面的可选部分“关于物理的其他事项”):
-
重力:定义重力加速度的向量。默认情况下,它仅在 y 轴上具有负值。特别是,9.81 的绝对值对应于地球上的重力,如果我们让 Unity 的单位是米。
-
默认材质:所有未设置物理材质的碰撞体和刚体所使用的物理材质(我们将在后面了解更多关于物理材质的内容)。
-
睡眠时间:Rigidbody 2D 进入睡眠状态之前需要经过的时间,这意味着它不再由物理引擎更新(我们将在后面了解更多关于刚体及其睡眠的内容)。
-
线性睡眠容忍度:这是在睡眠时间后刚体进入睡眠状态的线性速度。想象一下,你的游戏中有很多对象正在减速,现在它们的速度非常慢,以至于玩家几乎无法感知它们的运动。因此,在物理引擎中持续更新它们是浪费计算资源。因此,这个变量限制了对象在进入睡眠状态(不再从物理引擎接收更新)之前可以有的最低速度。这个值越低,模拟将越精确,但如果有很多对象以高于这个速度移动,成本也会更高。
-
角速度睡眠容忍度:在睡眠时间后,刚体进入睡眠状态的角速度。想象一下之前相同的情况,但这次身体旋转得非常慢。同样的推理适用:值越低,模拟越精确,但通常成本也更高。
-
层碰撞矩阵:这决定了哪些类型的对象会相互碰撞。默认情况下,所有选项都被选中,但你可能希望让两种特定的对象之间不发生碰撞。对象是根据它们所在的物理层来区分的。如果你还记得,我们在第一章“Unity 中的二维世界”中讨论了层和标签。因此,通过层菜单,你可以创建新的层,在检查器中,你可以将层分配给特定的游戏对象。为了使事情更简单,层碰撞矩阵在下面的截图中有展示:

对于我们的游戏,我们可以保留所有默认值,因为它们对我们所需的功能来说已经足够好了。
物理组件
Unity 的物理引擎通过组件工作。一些描述对象本身的属性,一些描述相互之间的关系,还有一些甚至在游戏世界的某个区域内。
它们可以按以下方式划分:
-
刚体:在物理引擎中定义刚体的组件
-
碰撞体:为刚体定义物理形状
-
关节:对刚体施加一个或多个约束
-
效应器:在游戏世界的某个区域内改变物理属性,影响该区域内所有的刚体

Unity 2D 物理引擎不同组件的总结图
现在,让我们更详细地看看它们。
刚体
刚体,与碰撞体一起,是 Unity 物理引擎的核心。当它们附加到游戏对象上时,它们将游戏对象置于物理引擎的控制之下,物理引擎将负责正确地移动其变换。实际上,它们应该通过其他函数来移动,脚本不应该直接操作变换。我们将在稍后详细讨论这一点。
组件的确切名称是Rigidbody 2D(而Rigidbody用于 3D 物理引擎,但为了简洁,我们经常使用术语刚体来指定Rigidbody 2D组件)。
注意
从 Unity 5.4 开始,然后在 Unity 5.5 中,Rigidbody 2D组件略有变化。实际上,Unity 的物理引擎已经进行了许多改进。
一旦将组件添加到游戏对象中,它看起来就像这样:

Rigidbody 2D 的工作原理
每次我们想要定位一个对象(或其子对象)或移动它时,我们都会改变其变换,这定义了它在空间中的位置以及它是如何旋转或缩放的。然而,物理引擎将模拟对象在模拟物理的世界中的交互。因此,如果对象与另一个对象发生碰撞,它将改变方向或速度。这意味着物理引擎必须以某种方式改变对象的变换(这就是为什么我们不应该用脚本触摸物理对象;我们稍后会看到如何通过脚本处理它们)。这种方式是刚体组件,它是一种在物理引擎和对象的属性(包括变换)之间的枢纽。
因此,在物理引擎完成计算后,它会与刚体通信,刚体需要处于其下一个位置,而刚体组件提供改变变换以匹配新位置。
同样的规则适用于碰撞体(我们很快会详细讨论)。每个连接到刚体相同对象(或其子对象之一)的碰撞体都将与刚体链接,我们不应该修改碰撞体或移动它,而应该移动整个刚体。这些与刚体链接的碰撞体实际上允许刚体与另一个刚体的碰撞体发生碰撞,并在物理世界中赋予它们形状。
身体类型
在Rigidbody 2D中最重要的变量是身体类型,以下屏幕截图中已突出显示:

注意
如果你使用的是 Unity 的早期版本(低于 5.5),这个变量将不可用。然而,你仍然可以通过检查Is Kinematic参数来获取运动学模式,该参数在旧版本中可用(从 5.4 及以下版本)。你可以在以下屏幕截图中看到这一点(Unity 5.4):

在 Unity 5.3 中也是如此:

旧版本,如 5.2 及以下版本,与 5.3 类似。
实际上,根据身体类型的设置,它会影响组件上可用的其他设置。此外,重要的是要记住,任何连接到Rigidbody 2D的Collider 2D都会继承Rigidbody 2D组件的身体类型。
身体类型决定了对象将如何移动,碰撞体将如何交互,因此也决定了刚体将有多大的计算成本。
在运行时更改Rigidbody 2D的Body Type可能很复杂。你需要考虑一些事情;例如,当Body Type发生变化时,与质量相关的各种内部属性会立即重新计算。此外,附加到 Rigidbody 2D 的 Collider 2D 组件的所有现有接触都需要在 GameObject 的下一个FixedUpdate期间重新评估。因此,根据附加到身体的接触和 Collider 2D 组件的数量,当你更改刚体的Body Type时,可能会引起性能的变化。
注意
值得注意的是,有时刚体被描述为相互碰撞。虽然当我们谈论物理时(因此当我们谈论刚体时)这是真的,但在 Unity 的 Rigidbody 2D(组件)的情况下并不成立。实际上,在 Unity 的物理引擎中,只有附加到刚体上的碰撞体才会发生碰撞。然而,说两个刚体发生了碰撞,只是说它们的碰撞体相互碰撞的简短说法。
Body Type可以设置为三种类型之一。让我们详细看看:
-
动态: 这意味着刚体将遵循所有动态计算,这意味着处理导致运动的力。实际上,刚体将具有质量,以及线性和角动量阻力。此外,身体将受到重力的影响。实际上,这是默认的身体类型,因为它是最常用的,并且与所有东西发生碰撞。但正因为如此,它也是计算成本最高的身体类型。
-
运动学: 这意味着物体仍然能够作为一个物理对象移动,但没有运动所需的力,因此也不受重力的影响。实际上,你需要用任何公式(我们稍后会看到)来编写它的运动脚本,这可能或可能不是物理上现实的(或者也许是在你的游戏世界中)。然而,它仍然能够发生碰撞,这意味着物理引擎将通知你的脚本刚体已经发生了碰撞,然后接下来发生什么就取决于我们了。在遇到动态类型的物体时,运动学类型的物体被认为是不可移动的,这意味着具有无限的质量。实际上,所有动态属性,如质量,都是不可用的。从计算的角度来看,运动学类型的物体比动态类型的物体更快,因为不需要计算所有动态力,这要求物理引擎的资源更少。
-
静态: 这意味着刚体在物理引擎(或模拟)下不应该移动。这适用于具有无限质量的物体。在模拟下,静态刚体 2D 被设计为不移动。如果发生碰撞,静态刚体 2D 的行为就像一个不可移动的物体(好像它具有无限质量)。它也是使用最少的资源密集型身体类型。静态身体只与动态刚体 2D 发生碰撞。不支持两个静态刚体 2D 发生碰撞。这仅仅是因为它们不是设计来移动的。因此,对于这种身体类型,可用的属性数量有限。
为了更好地理解这些身体类型之间的差异,这里有一个表格,列出了每个身体类型可用的不同功能(我们将在下一节中详细了解它们):

刚体属性
在前面的表格中,我们看到了许多属性;它们可能对某些身体类型可用,也可能不可用。但它们实际上做什么,以及它们为刚体确定什么?让我们详细探讨这些属性:
-
材料: 一种物理材料,用于确定碰撞属性,例如摩擦和弹跳(我们将在后面了解更多关于物理材料的内容)。此材料将应用于受刚体控制的全部碰撞器(我们将在下一节中了解更多关于碰撞器的内容)。当你需要具有相同物理材料的许多碰撞器时,这很有用。
-
模拟: 一个复选框,用于启用刚体与物理引擎的交互。如果未勾选,则刚体以及所有引用它的碰撞器将禁用并透明,对物理引擎来说就像它们不存在一样。这在运行时启用和禁用许多碰撞器时很有用(请参阅可选的“更多关于物理”部分以了解更多信息)。
-
使用自动质量: 一个复选框,如果启用,允许 Unity 自行计算物体的质量。这些计算基于每个引用该特定刚体的碰撞器的尺寸和密度。
-
质量: 如果前面的复选框被禁用,我们可以手动为我们的刚体指定一个质量。
-
线性阻力: 影响刚体的线性阻力值。如果它不为零,物体最终会停止移动。
-
角阻力: 影响刚体的角阻力值。如果它不为零,物体最终会停止旋转。
-
重力比例:这是特定刚体的重力值的乘数。这意味着
0.4的值将重力减少到原始值的 40%。这在你的游戏中,当有对象以不同的方式对重力做出反应时很有用。想象一下你的法师发出的火球;你可能希望它有一个相当直的轨迹(即使它仍然是一个抛物线,除非轨迹完全直线且重力比例设置为0,这意味着没有重力影响刚体)。而你可能希望你的士兵的手榴弹有一个清晰的抛物线轨迹,因为它受到重力的影响。大于1的值会导致比原始值更强的重力,如以下图所示:![刚体属性]()
在前面的图中,左侧是一个火球,当重力比例设置为零时,它不受重力影响,导致直线轨迹。右侧是一个手榴弹,它受到重力的影响(因为重力比例大于零;在这种情况下,它正好是
1,这意味着正常重力)。在没有其他力的作用下,具有线性速度并受到重力影响的刚体具有抛物线轨迹。 -
使用完整运动学接触:仅适用于运动学身体类型的复选框。如果启用,它允许运动学刚体与其他运动学刚体发生碰撞。默认情况下,它设置为 false,这意味着刚体将仅与动态刚体(除了设置为触发器的碰撞器)发生碰撞。
-
碰撞检测:Unity 检测碰撞的方式。它可以是离散或连续的。在前一种情况下,碰撞检测仅基于物理对象的当前位置计算,如果它们发生碰撞(这意味着如果对象位置更新后碰撞器重叠,则 Unity 计算碰撞)。相反,连续碰撞检测是基于轨迹本身,而不仅仅是物体的位置。想象一下,你有一个非常快的子弹,它正对着一堵非常薄的墙。由于游戏是离散的(游戏逐帧渲染),当子弹靠*墙时,在下一个更新中,它可能移动得太快,以至于它的新位置在墙的后面。因此,使用离散碰撞检测时,子弹可以无问题地穿过墙,因为物理引擎没有检测到任何碰撞。相反,使用连续碰撞检测时,物理引擎知道子弹所跟随的轨迹,并对它进行计算。因此,即使子弹的最终位置在墙后面,碰撞也会被检测到,并且由物理引擎正确处理,如以下图所示:
![刚体属性]()
在前面的图中,左侧是离散方法,其中只考虑帧之间的不同位置。因此,如果子弹在下一帧的位置在墙后面,离散方法将不会检测到碰撞。右侧是连续方法,其中考虑了子弹的整个轨迹。因此,即使子弹在下一帧的位置在墙后面,也会检测到碰撞,并基于碰撞计算一个新的位置。第二种方法在计算上稍微昂贵一些。
-
睡眠模式:这是 Unity 处理刚体在开始时应该处于唤醒状态还是睡眠状态,或者是否有睡眠可能性的方式。这个变量的可能选择,具有自解释的名称,包括:永不睡眠、开始唤醒和开始睡眠。当刚体没有被物理引擎完全考虑时,它处于睡眠状态(这与没有被模拟不同,现在我们知道了原因)。想象一下,你的游戏中可能有多少物体正在有力地移动,但此刻它们并没有移动。例如,想象一个有数千个球的水球池,但此刻没有任何一个球在移动。在这种情况下,在物理引擎中对每个球进行空计算是无用的。另一个例子:想象一个停止摆动的摆锤,其摆动弧线太小,玩家无法感知。计算摆锤在这个弧线上的确切位置是浪费计算资源。更好的做法是停止摆锤或球(将它们设置为睡眠模式),直到发生某个事件,例如玩家跳入水球池或推动摆锤。因此,出于性能考虑,并非所有刚体在任何时刻都是唤醒状态。然而,它们可以通过事件被唤醒,这在物理引擎中通常是自动的。但也可以通过脚本由你控制(我们将在下一节中看到更多关于这个的细节)。
-
插值:当一个刚体在移动时,可能受到力的作用,物理引擎会对其下一个位置进行一些计算。然而,物理引擎并不完美,无法复制我们的物理。事实上,算法会受到数值不稳定性的影响,这可能导致我们案例中的运动出现颠簸。因此,Unity 为你提供了两种*滑运动并使其不那么颠簸的方法,以及无选项,其中不执行任何*滑。第一种方法,称为插值,考虑了刚体的先前位置。相反,第二种方法,称为外推,考虑了对象的下一个位置的预测。两种方法都很好,你可以感知它们与无之间的区别。然而,两种方法之间的区别可能难以理解,特别是它们的行性行为非常相似,有时理解哪种更适合你的游戏可能只能通过试错(除非你有非常具体的需求)。
![刚体属性]()
在左侧,轨迹没有插值;这导致轨迹碎片化。在右侧,轨迹被插值,其曲线被*滑化。
-
约束:这些可以防止刚体以某种方式(如果不是完全)移动或旋转。在 2D 的情况下,你可以冻结沿x或y或两个轴的运动,以及沿z轴的旋转。所有这些都是独立的复选框,可以以任何组合选择。当然,选择所有这些意味着刚体将无法移动。想象你正在开发一个益智游戏,你的主要角色需要移动一个盒子,可能通过推它。然而,我们不想让盒子在没有推到盒子的中间点时开始自身旋转。因此,我们可以冻结盒子的旋转,同时仍然允许盒子移动并被主要角色推挤。

在左侧,没有约束,因此当玩家推挤时,物体可能会旋转。在右侧,旋转被冻结,当玩家推挤时,物体不会旋转。选择哪种行为取决于你游戏的设计。
注意
信息:一个展开图,显示了刚体的所有其他变量,这在调试中非常有用。
处理刚体
现在我们已经详细探讨了 Rigidbody 2D 组件,让我们看看我们如何在脚本中处理它。如前所述,脚本不应该改变刚体的 Transform。那么我们如何移动它们呢?答案是存在一些特殊函数。
这些函数需要在刚体上调用;因此,你可能想要有一个 Rigidbody 2D 的引用,如下面的脚本片段所示:
*//Reference to the RigidBody2D component *
public Rigidbody2D rb2D;
void Start() {
* //Get the reference to the Rigidbody2D component*
rb2D = GetComponent<Rigidbody2D>();
}
因此,你可以这样调用函数:
Rb2D.NameOfTheFunction()
当然,这里 NameOfTheFunction 将被下面列出的函数之一所替代。然而,如果你记得,物理引擎可能会在每一帧被调用多次,因此所有这些函数都应该在 FixedUpdate() 中调用。
那么,我们实际上如何移动一个刚体呢?对于动态刚体类型,我们可以让它们在外部力(如重力、碰撞等)的 mercy 下自由移动,或者我们可以施加一个特定的力。
为了向刚体施加力,有以下有用的函数:
-
AddForce(Vector2 force, ForceMode2D mode = ForceMode2D.Force): 应用在force参数中指定的力。此外,枚举ForceMode2D是一个可选参数,用于指定力是否应该作为冲量作用。默认情况下,它不是冲量。 -
AddForceAtPosition(Vector2 force, Vector2 position, ForceMode2D mode = ForceMode2D.Force): 与前面的函数行为类似,但你可以指定力的应用点。 -
AddRelativeForce(Vector2 relativeForce, ForceMode2D mode = ForceMode2D.Force): 与第一个函数行为类似,但力是在局部坐标系中指定的。 -
AddTorque(float torque, ForceMode2D mode = ForceMode2D.Force): 向刚体施加扭矩;同样,第一个函数施加的是力。
对于运动学刚体类型,我们有两个函数来明确移动这些类型的刚体,同时仍然允许物理引擎正确执行碰撞检测。在这些函数内部,我们直接传递一个位置和一个旋转,这些可以通过任何公式计算得出。因此,它们可以遵循你想要的任何物理定律(或游戏中有意义的定律):
-
public void MovePosition(Vector2 position): 将刚体移动到指定的位置 -
public void MoveRotation(float angle): 将刚体旋转到指定的角度
注意
最简单的例子是将经典运动学定律应用于速度,使运动学刚体无限期地沿直线移动。对于最好奇的你们,公式如下:

在物理学中,∆(读作 delta)表示最终值减去初始值,可以通过显式分离空间的变化来重写为:

我们可以用以下代码片段将此翻译成 Unity(而不是 Time.deltaTime,我们需要使用 Time.deltaFixedTime):
public Vector2 velocity;
void FixedUpdate () {
rb2D.MovePosition(rb2D.position + velocity * Time.fixedDeltaTime);
}
此外,我们为所有类型的刚体都有一些或另一些函数:
-
IsAwake(): 如果刚体处于唤醒状态,则返回 true。 -
IsSleeping(): 如果刚体处于休眠状态,则返回 true。 -
IsTouching(Collider2D collider): 如果碰撞体接触刚体(这意味着刚体上附加的任何碰撞体),则返回 true。 -
OverlapPoint(Vector2 point): 如果点与刚体重叠(这意味着刚体上附加的任何碰撞体),则返回 true。 -
Sleep(): 使刚体进入休眠状态 -
WakeUp(): 禁用刚体的睡眠模式
注意
要查看 Rigidbody 2D 组件的完整函数和变量列表,您可以在此处查阅官方文档:docs.unity3d.com/ScriptReference/Rigidbody2D.html。
碰撞体
想象一下,你的游戏中的女主角在一个复杂的环境中移动;这里的复杂是指详细的,例如,国王城堡的厨房。如果物理引擎非常逼真,它应该考虑女主角的手可以越过桌子,只要她的身体保持在侧面。然而,这在实时运行中是不可能的(或者至少对于玩家当前的硬件来说是不可能的)。因此,碰撞体为需要某种物理交互的对象和角色提供了一个*似的形状,例如碰撞。例如,角色通常被*似为胶囊形状的碰撞体,而具有球形或盒形碰撞体的物体。当然,如果需要更详细的级别,不同的碰撞体可以充当更复杂的碰撞体。最终,3D 模型的全部多边形(或 2D 精灵的周界)都可以成为碰撞体的一部分,从而获得更高的真实感,但代价是性能。因此,你想要确保在每种情况下都保持正确的细节级别。
在 Unity 中,碰撞体可以是五种类型,它们是:
| 名称 | 在检查器中的显示 | 描述 |
|---|---|---|
| 二维圆形碰撞体 | ![]() |
碰撞体的形状是一个圆形,由局部坐标系中的一个位置和一个半径定义。 |
| 二维盒子碰撞体 | ![]() |
碰撞体的形状是一个与世界轴(x 和 y)对齐的矩形,它在局部坐标系中由宽度和高度定义。 |
| 二维多边形碰撞体 | ![]() |
碰撞体的形状由由线段组成的自由形状边缘定义。这样,你可以调整它以非常精确地适应 Sprite 图形的形状。请注意,此碰撞体的边缘必须完全包围一个区域(与二维边缘碰撞体不同)。 |
| 二维边缘碰撞体 | ![]() |
碰撞体的形状由由线段组成的自由形状边缘定义。这样,你可以调整它以非常精确地适应 Sprite 图形的形状。与二维多边形碰撞体不同,其边缘不一定包围一个区域。例如,可以创建一个N形状或只是一条直线。 |
| 二维胶囊碰撞体 | ![]() |
碰撞体的形状是一个无顶点的胶囊(因此它很难卡在顶点或其他碰撞体的角落之间)。此外,它是一个实体碰撞体,所以如果一个物体完全在这个碰撞体内部,它会被推出来并被检测为碰撞。 |
它们都共享相同的核心设置,以及一些额外的设置来定制碰撞体的形状。例如,对于二维圆形碰撞体,我们可以决定中心点和半径。然而,Unity 会自动尝试将精灵或 3D 模型包含在碰撞体内部,尽管可能需要手动调整(有关在视觉上编辑碰撞体的方法,请参阅以下内容)。
注意
由于本书专注于 2D 世界,为了简洁起见,我们将省略碰撞体的 2D 后缀,假设我们总是指代 2D 组件,而不是具有相同名称的 3D 组件。例如,二维盒形碰撞体变为盒形碰撞体。
让我们来探索每个碰撞体的核心选项:
-
密度:这会影响所引用的刚体的质量。零值意味着碰撞体将在刚体质量计算中完全被忽略。值越高,碰撞体对刚体质量的贡献就越大。
注意
如果你没有看到密度选项,这是正常的。实际上,只有当关联的刚体(即附着在同一个游戏对象上的刚体或其层次结构中的某些父级)上的自动质量选项被启用时,它才会可见。实际上,当刚体组件自动计算质量时,它会考虑所有连接到该刚体的碰撞体及其密度。
-
材料:一个物理材料,它决定了碰撞的性质,例如摩擦和反弹(我们将在后面了解更多关于物理材料的内容)。
-
是否触发:如果您希望碰撞体作为触发器行为,请勾选此选项。这意味着碰撞体不会用于执行碰撞,而是当另一个碰撞体进入此碰撞体时触发某些操作(有关更多详细信息,请参阅下一节)。
-
由效应器使用:如果您希望碰撞体被附加的效应器使用,请勾选此选项(我们将在后面了解更多关于效应器的内容)。
-
偏移:这是以局部坐标表示的碰撞体几何形状的偏移量;换句话说,碰撞体在附着游戏对象的位置上x和y方向上应该离多远。
-
信息:一个展开的窗口,显示碰撞体的所有其他变量。这在调试中非常有用。
然后,还有每个碰撞体的特定选项:
-
半径:仅用于圆形碰撞体,它决定了圆的半径。
-
大小:这仅用于盒形和胶囊形碰撞体。在前者的情况下,它决定了盒子的大小,在后者的情况下,它决定了胶囊填充的盒子的大小(因此间接决定了胶囊的大小),如下面的图所示:
![碰撞体]()
-
点:这是多边形或边缘碰撞体的不可编辑信息,关于它们的复杂性。它描述了碰撞体的所有点以及它们如何连接成路径。
-
方向:这仅用于胶囊碰撞器,可以设置为垂直或水*。这控制胶囊的方向;具体来说,它定义了半圆形端盖的位置。
此外,您还可以通过点击以下图标来直观地编辑碰撞器,该图标在所有碰撞器中都是可用的:

因此,您可以直接在场景视图中修改碰撞器。这相当直观,所以我会让您自己探索这个功能。
小贴士
您可以通过点击边缘之一为边缘或多边形碰撞器创建顶点。
处理碰撞器
通常,在视频游戏中,当碰撞器碰撞时,我们希望触发一些动作并运行特定的代码。例如,想象一颗子弹击中墙壁的表面,我们可能想在那个点实例化一个粒子效果并移除子弹。同样的情况也适用于火球,但会有火效果。或者想象玩家穿过一扇门并触发一个场景。在这种情况下,有一个体积触发器(一个将is Trigger设置为 true 的碰撞器),当玩家进入时,体积触发器会触发场景。另一个例子是一个充满有毒气体的区域,玩家在该区域停留的时间越长,他的健康就会逐渐减少。
所有这些都可以通过使用一些特殊函数来实现。如果您还记得,在第二章中,烘焙纸杯蛋糕塔,我们看到了从 monobehaviour 派生的每个脚本都可以有一些函数,例如Start()和Update(),这些函数会被 Unity 自动调用。在这些函数中,有一些是由 Unity 的物理引擎调用的。
对于触发体积,有以下函数:
-
OnTriggerEnter2D(Collider2D other): 当前者进入后者时,在刚体和体积触发器上都会调用此函数。其他碰撞器的信息作为参数通过other变量传递。 -
OnTriggerStay2D(Collider2D other): 这在刚体和体积触发器上每帧都会被调用,只要前者在后者内部。其他碰撞器的信息作为参数通过other变量传递。 -
OnTriggerExit2D(Collider2D other): 当前者离开/退出后者时,在刚体和触发体积上都会调用此函数。其他碰撞器的信息作为参数通过other变量传递。
对于未设置为触发器的碰撞体,有类似的功能。然而,不是只有关于另一个碰撞体的信息,通过Collision2D类可以获得关于整个碰撞的信息,该类除了碰撞体之外还存储了接触点、相对速度等信息。更多关于这方面的信息可以在官方文档中找到:docs.unity3d.com/ScriptReference/Collision2D.html
因此,碰撞体的函数是:
-
OnCollisionEnter2D(Collision2D coll): 当刚体/碰撞体刚刚接触(发生碰撞)时,这个方法会被调用。关于碰撞的信息可以在coll变量中找到。 -
OnCollisionStay2D(Collision2D coll): 当两个刚体/碰撞体持续接触时,每一帧都会调用这个方法。关于碰撞的信息可以在coll变量中找到。 -
OnCollisionExit2D(Collision2D coll): 当两个刚体/碰撞体停止接触时,这个方法会被调用。它们碰撞的信息可以在coll变量中找到。
在我们的特定情况下,我们需要这些函数(触发器函数)来检测当糖果击中敌人时,减少他们的健康,并最终触发正确的动画。我们将在本章的末尾看到这一点。
关节
到目前为止,我们讨论了刚体和碰撞体,但它们只是单个物理体。那么更复杂的机械系统呢?想象一下一根绳子,它正拉着装有骨架和钥匙的笼子。所以,我们的主人公决定切断绳子,让笼子掉落。一旦绳子被切断,笼子就会掉下来。然而,即使在绳子被切断之前,笼子就已经受到重力的作用。事实上,如果我们的主人公推笼子而不是切断绳子,那么笼子就会开始振荡。原因是绳子给笼子施加了一个约束——它只允许在半径等于绳子长度的圆(或如果我们在 3D 中,则是球)内移动。当然,当它振荡时,重力使得笼子在圆(或球)的边缘移动。如果绳子是一根刚性的金属棒呢?那么笼子会有更严格的约束,因为现在它被迫只位于圆(或球)的边缘。如果绳子是弹簧呢?这又是一种约束,如以下图所示:

尽管笼子受到重力的作用,但绳子施加了一个反作用力。因此,笼子不会掉落。因此,绳子对笼子施加了一个约束。特别是,它限制了笼子的运动,使其保持在半径等于绳子长度的圆内。
现在,想象一下将这些约束放在一起。例如,你将笼子系在一个悬挂的弹簧上,从笼子的底部拉出一根绳索,绳索悬挂着一个金属球。系统将开始变得更加复杂。但 Unity 是如何处理所有这些的呢?答案是,使用关节组件。
注意
注意,所有以 2D 结尾的关节都属于 2D 物理引擎;否则它们属于 3D 物理引擎。所以请小心,不要混淆或连接错误的关节。然而,从现在开始,我们将始终将其称为 Joint 2D,即使没有 2D,除非另有说明。这个决定是为了使章节更清晰、更流畅地阅读。
关节的主体属性
通过使用关节组件,你可以将一个刚体连接到另一个刚体上,以便为它们提供特定的约束,同时仍然为运动留下一些自由度。特别是,Unity 提供了九个 Joint 2D 组件。但在我们逐一介绍它们之前,让我们先探索一下 Unity 中关节的一些通用属性。
另一个刚体
正如我们之前所说的,一个关节涉及两个刚体(目标关节 2D除外)。第一个是连接到关节相同物体的。另一个可以任意选择。因此,在关节组件内部有这两个选项:
-
启用碰撞:两个刚体都将有碰撞器。如果这个切换为真,这意味着两个刚体将根据物理引擎和我们在前两个部分中看到的进行碰撞。大多数情况下,具有关节的刚体属于同一个大系统,你不想它与自己碰撞。因此,默认值是 false。
-
连接的刚体:这是对关节第二个刚体的引用,正如其名所示。
然后,大多数的关节(除了相对关节 2D和目标关节 2D)需要两个应用关节的点。再次想象一下笼子悬挂在绳索上的例子。绳索连接到笼子的位置是一个重要因素,因为如果绳索连接到笼子的一个角落,这将导致旋转,如图所示:

我们将绳索系在了笼子上,这是很重要的,因为它将影响笼子对力的反应。然而,值得注意的是,无论你在哪里附加一个刚体,绳索的线始终会通过刚体的质心(重心)。
此外,你需要考虑可能存在其他可能移动笼子的力,而绳索连接的位置是一个重要的信息。同样适用于另一个刚体;在笼子的例子中,另一个刚体是天花板。
因此,有一些选项可用于确定这两个应用点,在 Unity 中被称为锚点:
-
自动配置连接锚点:如果勾选了这个选项,Unity 将负责确定两个锚点的位置。当然,如果你已经有一个特定的关节应用点并且想要在那里放置锚点,请将其关闭。
-
锚点:相对于刚体,关节在连接的刚体上的锚点的 x 和 y 位置。
-
连接锚点:这是相对于另一个物体的刚体,在连接刚体变量指定的刚体上,关节锚点的 x 和 y 位置。
破坏关节
回到悬挂笼子的例子,想象一下,英雄决定尝试跳到笼子的顶部,然后再跳到另一个*台上。然而,他没有考虑到绳子真的很旧(实际上笼子里有一具骨架!),一旦他落在笼子上,绳子断裂,导致笼子和我们的英雄一起坠落。因此,确实,关节施加了约束,但它们也有局限性。出于同样的原因,如果你用太大的力拉扯橡皮筋,橡皮筋最终也会断裂。
在 Unity 中,关节也可以被破坏。特别是,在它们的组件中,有两个变量:
-
断裂力:所有关节都有这个属性,它表示关节断裂后的力数值。在 Unity 中破坏关节意味着从对象中删除关节组件。默认情况下,它被设置为无限大,这意味着关节是不可破坏的。否则,数值越低,关节越容易断裂。在笼子的例子中,如果我们选择为模拟绳子的关节设置一个非常低的值,那么绳子很可能真的很旧,当我们的英雄跳到笼子的顶部时,他们两人都会掉进城堡的地牢中。
-
断裂扭矩:除了二维距离关节、二维弹簧关节和二维目标关节之外的所有关节都有这个属性。它表示关节断裂后的扭矩数值。正如我们之前所看到的,刚体不仅可以移动,还可以旋转。默认情况下,它被设置为无限大,这意味着关节是不可破坏的。否则,它可以被设置为有限值,允许关节在超过指定值的扭矩下断裂。
注意
对于一个在扭矩作用下断裂的关节的例子,你可以想象一些正在旋转但运动被阻止的东西,比如当你使用螺丝刀并继续直到木头(作为约束)断裂时。然而,这里还有一个不那么直观但更常见的例子(甚至在视频游戏中)。想象你把自己附加在架子/边缘上。由于你的附加点与架子的质心之间有一个偏移(如果架子是*的,它们很可能是垂直的),架子上会施加一个扭矩。在扭矩的作用下,架子应该围绕其质心旋转,但由于墙壁阻止了它,它并没有旋转。如果你足够重,这意味着你施加在架子上的力更大(或者如果你以某种方式能够增加偏移),扭矩会增加,直到达到断裂点。然后约束将断裂,这可能导致架子或墙壁断裂,具体取决于哪一个有更大的阻力(最可能的是架子会断裂,而墙壁仍然站立)。以下图表应该有助于你理解这个例子:

特定关节
到目前为止,我们只看到了 Unity 中关节的一般特性,但现在我们将更深入地了解所有九种 2D 关节组件。
值得注意的是,关节分为两类:弹簧关节和电机关节。前者使用弹簧施加约束,可能完全刚硬,以模拟刚性杆。电机关节则可以主动对刚体施加力。一些关节既是弹簧又是电机,例如带有电机的弹簧。
小贴士
Unity 中的所有关节都有一个图标(例如,当你将组件放置在 检查器 中时可见)。在这本书中,你可以在关节图像的右上角看到这个图标(在下一节中)。这个图标非常有用,可以帮助你记住关节的作用以及它是如何工作的。因此,在阅读下一节或编写你的游戏时,请注意这个图标,它可能有助于你更好地理解关节。
距离关节 2D
此关节保持两个刚体之间的特定距离。此关节的目的是在游戏世界中保持两个刚体或一个刚体与固定点之间的特定距离。实际上,如果你将 连接刚体 变量设置为 None,你可以在 连接锚点 中指定固定点的位置。
它应该看起来像以下这样在 检查器 中:

此关节向两个刚体(或如果使用固定点,则仅向其附加的刚体)施加线性力,使用一个非常刚硬的模拟弹簧(不可配置)以保持距离。该关节不施加任何扭矩。
除了我们在上一节中看到的参数之外,二维距离关节还有以下选项:
-
自动配置距离:如果设置为 true,Unity 将计算两个刚体(或刚体和固定点)之间的当前距离,并将其值放置在距离变量中。
-
距离:指定两个刚体(或刚体和固定点)不能超过的距离。
-
仅最大距离:如果启用,两个刚体(或刚体和固定点)之间的距离可以小于在距离中指定的值。如果此选项被禁用,则距离是固定的,两个点不能比距离更远或更*。
备注
记住,如果关节的断裂力是一个有限值,即使将仅最大距离选项设置为 false,它最终也会断裂。
当仅最大距离为 false 时,两点之间的约束是刚性的,所以你可以想象它们通过一个金属绳索/杆和铰链连接在一起(因为它们仍然可以相对于彼此旋转)。这种用法的一个例子是当你需要将火车上的两个沙发连接在一起时,因为它们不能比特定的距离更远或更*。如果仅最大距离为 true,那么它们可以比距离更*,但不能更远。这是我们的绳索的行为,它支撑着笼子,可以靠*绳索系的地方,但不能更远。另一个例子是拴着绳索的狗。
然而,重要的是要记住,两个刚体都可以相对于彼此自由旋转。实际上,此关节仅对两个刚体的相对位置施加约束。
二维固定关节
此关节的目的是在游戏世界中保持两个刚体或刚体和固定点之间的一定相对偏移(线性和角偏移)(你可以通过将连接刚体设置为None来指定它)。
它在检查器中应如下所示:

此关节同时施加一个线性力来补偿线性偏移,以及一个扭矩来补偿角偏移。与二维距离关节类似,它使用一个非常刚性的模拟弹簧,但你也可以调整弹簧的值,例如频率。
因此,除了我们在上一节中看到的参数之外,二维固定关节还有以下选项:
-
阻尼比:这定义了弹簧振荡被抑制的程度。其值范围从 0 到 1;值越高,运动越少。正如你所期望的,弹簧会超过期望的距离然后反弹回来,导致弹簧振荡。阻尼比决定了振荡被阻尼(减少)的速度,从而决定了弹簧回到其静止位置的速度。
-
频率:这定义了当刚体达到分离距离时弹簧振动的频率。它以每秒周期数来衡量,其值范围从 1 到 100 万。值越高,弹簧越硬,这意味着运动越少。值得注意的是,零值意味着弹簧完全僵硬。
另一种思考这个关节的方法是将 GameObject 在层次结构中视为父级,这样子级就可以相对于父级固定。然而,这个关节为你提供了比简单父级更多的选项,包括打破它的可能性。
这个关节的使用示例之一是当你有一系列刚体(例如从天花板悬挂的真实链条,或者你可以想象由部分组成的桥梁),并且你想要将它们刚性连接在一起。优点是你可以允许关节有一定的灵活性,因此,在桥梁的情况下,它仍然可以在你设定的限制内稍微弯曲。
2D 摩擦关节
这个关节的目的是通过减慢两个刚体之间或刚体与游戏世界中的固定点(你可以通过将连接刚体设置为None来指定)之间的运动来使线性偏移和角偏移都保持为零。
2D 摩擦关节在检查器中应该看起来像这样:

除了我们在上一节中看到的参数之外,2D 摩擦关节还有以下选项:
-
最大力:这决定了连接两个刚体的直线上的线性阻力。高值(最大值为 1,000,000)会产生强烈的线性阻力;因此,两个刚体不会在它们连接的直线上移动很多。相反,低值允许更多的运动。
-
最大扭矩:这决定了两个刚体之间的角阻力。高值(最大值为 1 百万)会产生强烈的角阻力;因此,两个刚体不会相对旋转很多。相反,低值允许更多的运动。
这个关节的使用示例之一是当游戏中存在需要摩擦力来显得逼真的物理对象时。想象一个*台,它固定在背景中的一个大轮子上。由于游戏是 2D 的,这个轮子只是一个美学元素;它实际上并不影响*台。因此,我们需要模拟*台和轮子之间的摩擦力,我们可以通过使用2D 摩擦关节来实现这一点。这样,我们可以在*台上产生一个角阻力;因此,它仍然可以旋转,但不是那么容易。也许,玩家可能会在*台的边缘放下一个重箱子,使其旋转足够,以便玩家可以通过。

左边是系统的表示。右边是当箱子被扔到*台上时会发生的情况,由于角摩擦,*台会缓慢旋转。
2D 铰链关节
此关节的目的是将刚体约束在围绕另一个刚体或空间中的固定点(如果将连接刚体设置为None,则始终由连接锚点指定)旋转。旋转可以是被动发生的(例如,响应碰撞或在重力作用下)或由电机主动提供扭矩,该电机为刚体提供扭矩。此外,可以设置限制以仅允许铰链在特定角度旋转,或允许围绕其轴旋转超过一整圈。
在检查器中看起来如下截图所示:

除了我们在上一节中看到的参数之外,Hinge Joint 2D还有以下选项:
-
使用电机: 如果启用,它允许关节在刚体上主动施加扭矩,模拟电机。
-
电机速度: 这指定模拟电机应以每秒多少度旋转。因此,值为 30 表示电机将在 12 秒内完成一整圈旋转(360/30=12)。
-
最大电机力: 这指定电机可以达到电机速度的最大力。想象一个非常重的物体需要更大的力来旋转并达到电机速度。如果电机不够强大,它将无法使刚体达到电机速度。此外,如果指定了断力矩,它可能会破坏关节。
-
使用限制: 如果为真,则关节限制刚体可以旋转的角度。
-
下角: 这设置由限制允许的旋转弧的下端。
-
上角: 这设置由限制允许的旋转弧的上端。
使用此关节的一个清晰的例子是与门一起使用。它们可以围绕铰链旋转,铰链将门与框架连接起来。我们可以限制门可以如何旋转,例如,你可能只想让它旋转 90 度。此外,如果门是自动的,我们可以模拟一个电机,该电机主动使门旋转。实际上,电机可能由脚本触发。最后,如果玩家想要将门推过其限制(例如,90 度),并且指定了断力矩,玩家可能用足够的力破坏铰链。

俯视图(鸟瞰图)。在左侧是系统的表示。在右侧是玩家推门超过其限制时会发生的情况。
相对关节 2D
这个关节使两个刚体保持基于彼此位置的相对位置。实际上,这个关节的目标与固定关节 2D相同;区别在于它们是如何实现的。固定关节 2D,正如我们之前看到的,是一种弹簧式的关节,只有当两个刚体处于指定的偏移和旋转位置时,它才会停止振荡,弹簧处于静止位置。相反,相对关节 2D是一种电机式的关节,它对刚体施加直接力和扭矩,使它们处于相同的偏移和旋转位置。
与固定关节 2D类似,相对关节 2D也可以与这两个一起工作:
-
两个刚体
-
一个刚体和一个固定点
要使用第二种情况,将连接刚体设置为None,然后在线性偏移变量中指定固定点的坐标。
它在检查器中显示如下:

除了上一节中看到的参数外,相对关节 2D还有以下选项:
-
最大力: 这指定了关节/电机可以用来校正两个刚体之间偏移的最大力。值越高,模拟的电机越能有效地校正偏移。默认情况下,最大力设置为
10000,这意味着一个非常强大的电机。 -
最大扭矩: 这指定了关节/电机可以用来校正两个刚体之间角偏移的最大扭矩。值越高,模拟的电机越能有效地校正角偏移。默认情况下,最大扭矩设置为
10000,这意味着一个非常强大的电机。 -
校正比例: 调整关节以确保其按预期行为。这可以通过增加最大力或最大扭矩来实现,这可能会影响行为(因此关节可能无法达到其目标)。因此,您可以使用此设置进行校正。默认设置
0.3通常是合适的(因为*均而言,关节的行为符合您的预期;这是一个可能通过试错找到的值)。但可能需要在 0 到 1 的范围内进行调整。 -
自动配置偏移: 当选中时,这将获取两个刚体的当前线性偏移和角偏移,将它们存储在线性偏移和角偏移变量中,并保持它们。
-
线性偏移: 这指定了两个刚体应具有的线性偏移,以局部坐标表示。
-
角偏移: 这指定了两个刚体应具有的角偏移,以局部坐标表示。
这种关节的一个使用示例是在游戏中的摄像机和 Avatar 之间。这样,摄像机可以稍微延迟地跟随 Avatar(因为如果摄像机是父级,它将与 Avatar 瞬间移动)。但它的振荡不会像固定关节 2D那样大,因为它是弹簧类型,可能会让玩家感到沮丧(当然,这也取决于固定关节 2D的配置,可能是一个真正的刚性弹簧;但通常在这种情况下使用相对关节 2D)。
这种关节的另一个典型使用示例是当某些东西应该跟随玩家时,例如她头上的生命计数器或她肩膀后面的友好精神。
滑块关节 2D
假设你想要限制刚体的运动仅沿一条线,这样它只能滑动到那条线上。这就是滑块关节 2D允许你做到的。至于其他关节,这条线可以在两个刚体之间,或者是一个刚体和游戏世界中的一个固定点之间。
它在检查器中看起来是这样的:

除了上一节中我们看到的参数之外,滑块关节 2D还有以下选项:
-
角度:这指定了刚体被约束保持的角度。在二维世界中,一个角度完全指定了一个方向,这个角度指定了运动受约束的方向。
注意
为了完全确定关节允许运动的那条线,还需要考虑刚体的当前位置。因此,一个点(刚体的当前位置)和一个方向(从角度变量指定)唯一地确定了一个运动受约束的线,如下面的图所示:
![滑块关节 2D]()
-
使用电机:如果为真,该关节还会使用模拟电机。
-
电机速度:这指定了电机应该达到的刚体速度。
-
最大电机力:这指定了电机为了达到刚体的电机速度所能使用/施加的最大力。
-
使用限制:如果为真,这允许对刚体沿线的某一段进行进一步的约束。
-
下限:这指定了刚体应从连接锚点处保持的最小距离。
-
上*移:这指定了刚体应从连接锚点处保持的最大距离。
你可能想要使用这种关节的典型场景包括滑动门,它可以上下移动,以及*台,它可以左右、上下甚至对角移动。

图中的滑动门沿着一段被约束
2D 弹簧关节
如其名所示,这是一个纯二维弹簧关节。它实际上模拟了连接在两个刚体之间,或刚体和固定点之间的弹簧。实际上,这个关节为你提供了弹簧关节的所有功能,因此你可以模拟所有其他纯(不带电机)的弹簧关节。
注意
事实上,二维距离关节是通过使用二维弹簧关节来模拟的,其中频率设置为0,阻尼比设置为1。
它在检查器中看起来如下所示:

除了上一节中我们看到的参数外,二维弹簧关节还有以下选项:
-
距离:这指定了两个刚体(或刚体和固定点)应保持的距离。你可以想象它也像弹簧在静止状态下的长度,这意味着没有施加力时弹簧的长度。
-
阻尼比:这指定了你想要抑制弹簧运动的程度。其值范围从 0 到 1。较低的值意味着可移动的弹簧,较高的值意味着硬弹簧。如果设置为 1,则弹簧不可移动。
-
频率:这指定了当物体接*指定的分离距离(即弹簧在静止状态下的长度)时,弹簧振动的频率(以每秒周期数衡量)。其范围从 0 到 1,000,000。数值越高,弹簧越硬。
无论何时你需要在你的游戏中放置一个弹簧,这个关节就是正确的选择。所以,一个例子可能是一个物理上逼真的发射台,你需要压缩弹簧,以便在跳跃时给它一个助推。然而,你不必一定将这个关节视为一个普通的弹簧。事实上,你可以将这个关节推向极限(例如一个非常硬的弹簧)。这样,你就能创建出在现实中没有弹簧,但在游戏中通过弹簧连接看起来很棒的其他行为。你玩过雷曼吗?这个角色的身体部位是分开的,但在游戏中它们仍然以协调的方式移动。如果你计划创建一个类似的角色,弹簧关节可以是一个将身体部位以逼真方式连接起来的有效解决方案。以下是雷曼的图像:

雷曼的图像。正如你所见,这个角色没有腿或手臂,但有手和脚。在你的游戏中,你可以通过一个弹簧关节将它们附着在胸部。
目标关节 2D
目标关节 2D是一种特殊的弹簧型关节,它没有第二个刚体,而是有一个目标。目标是保持组件附着在刚体上的距离一定距离。它只施加线性力;因此不会给刚体提供扭矩。
它在检查器中看起来是这样的:

由于此组件没有第二个刚体,有一些变量允许你指定目标:
-
锚点:在局部坐标系中定义,相对于刚体,关节附着在刚体上的位置。
-
目标:在局部坐标系中定义,相对于刚体,关节的另一端试图移动的位置。
-
自动配置目标:当勾选时,将目标设置为刚体的当前位置,这在我们的刚体被移动时很有用,可能是由其他力驱动的。此外,当选择此选项时,目标会随着刚体的移动而改变;相反,如果没有选择此选项,则不会改变。
此外,可以使用常规参数来控制弹簧的特性:
-
最大力:这指定了关节可以对刚体使用的最大力
-
阻尼比:这指定了弹簧运动被抑制的程度(有关更多详细信息,请参阅弹簧 2D 关节或固定 2D 关节部分)
-
频率:这指定了弹簧的频率(有关更多详细信息,请参阅弹簧 2D 关节或固定 2D 关节部分)
此关节的一个使用示例是在玩家需要用鼠标拖动对象时。在这种情况下,你可以将拖动的对象的目标设置为光标,这样它就会跟随光标,而不会有一个刚性的运动(就像我们会逐帧设置对象的位置到鼠标坐标一样)。此外,你可以使用锚点来指定对象应该连接的位置。例如,如果玩家从对象的角落开始拖动,你可以将锚点设置为那里,结果,对象将从那个点悬挂。
注意
你可以在上一章的某个练习中找到这个关节,以改善你游戏的外观。
轮关节 2D
轮关节 2D 是一种弹簧和马达类型关节的组合,它有非常特定的用途。正如其名所示,它模拟了车轮对物体施加的约束。特别是,它可以使用马达(车轮移动)旋转车轮,并使用弹簧模拟悬挂。
更具体地说,关节对两个连接的刚体施加线性力以保持它们在一条线上,一个角马达以在线旋转它们,以及一个弹簧来模拟车轮悬挂。
注意
有趣的是,你可以通过组合滑块关节 2D(两个使用马达和使用限制都关闭)和铰链关节 2D(使用限制关闭)来重建轮关节 2D。
它在检查器中看起来如下:

如您从前面的截图中所见,参数分为悬挂,它定义了一个弹簧和一个马达。两者都使用我们之前已经看到的相同参数(除了角度)和马达。
悬挂由以下定义:
-
角度:这指定了悬挂发生的角度(以世界坐标中的度数表示)。默认情况下,它设置为
90度,这意味着悬挂将向上发生,就像通常发生的那样(汽车的车身位于车轮之上,因此悬挂的方向沿着正y轴,这意味着 90 度),如下面的图所示:![轮轴 2D]()
在这个图中,我们可以看到汽车的轮子悬挂角度为 90 度,这意味着在游戏世界中向上。这是默认值。想象一下,如果你想有一辆能够通过磁力轮在天花板上行驶的汽车;你可能需要考虑改变悬挂的角度。在这种情况下,它与正常情况相反,因此角度将是 270 度。
-
阻尼比:这指定了弹簧运动被抑制的程度(有关更多详细信息,请参阅2D 弹簧关节或固定 2D 关节部分)。
-
频率:这指定了弹簧的频率(有关更多详细信息,请参阅2D 弹簧关节或固定 2D 关节部分)。
电机由以下定义:
-
电机速度:这指定了电机在轮子上应达到的速度
-
最大电机力:这指定了电机在轮子上施加的最大力,以便在轮子上达到电机速度
无需告诉你这个关节最常用的用途。每次在你的游戏中有一个轮子并且需要以逼真的方式移动时,这个关节就是你的最佳选择。
小贴士
如果你想创建一个由玩家控制的汽车,你可以将这个关节连接到车轮上,并将电机速度设置为0,以便通过你的脚本根据玩家的输入来控制这个变量。此外,你可以使用最大电机力来模拟不同的档位。
效果器
想象一下我们的英雄正在穿越一个魔法房间,因此他开始因为一个强大的咒语而漂浮。在这种情况下,当英雄在这个房间里时,重力应该被一种(魔法)力量所抵消。因此,我们需要指定在那个房间里,有一种使我们的英雄悬浮的力。
类似地,想象一下英雄在湖中扔下一个箱子。箱子不会以与它下落相同的速度下沉;此外,它可能会漂浮。因此,在我们的游戏中,我们需要指定湖所界定的区域具有特殊的物理属性。

游戏世界中有一些特殊的区域,那里的物理定律可能与其他游戏世界不同;在 Unity 中,这些区域可以通过效果器来指定
在 Unity 中,这可以通过使用作用力来实现。作用力是一些组件,它们会影响游戏世界中特定区域刚体的行为。它们之间真的很不同,因为它们做不同的事情,尽管核心概念是相同的:它们影响一个或多个进入它们控制区域的刚体。
作用力使用碰撞检测来知道它们区域内有哪些刚体,因此,正如我们之前提到的,刚体需要有一个碰撞器。否则,它们不会受到作用力的影响。
此外,作用力本身也需要它们自己的碰撞器才能工作。为了被作用力使用,它们应该将由作用力使用属性设置为 true。否则,作用力将不会影响任何刚体。作用力的碰撞器是否应该设置为触发器取决于作用力的类型以及你试图实现的目标,尽管有一个默认的方式去设置它(实际上,Unity 会在你以不寻常的方式使用作用力时给你一个警告)。
我们将逐个分析,这里有一个总结表,列出了作用力,说明了是否应该使用is Trigger:
| 作用力 | 链接的碰撞器应该是触发器吗? |
|---|---|
| 二维恒力 | 此组件连接到刚体,而不是碰撞器 |
| 区域作用力 2D | True |
| 浮力作用力 2D | True |
| 点作用力 2D | True |
| *台作用力 2D | False |
| 表面作用力 2D | False |
所有这些(除了二维恒力之外)都有几个变量来确定哪些刚体受到作用力的影响,这些变量是:
-
使用碰撞器掩码:如果启用,这允许你通过在下一个变量中指定的层来区分进入区域的碰撞器。
-
碰撞器掩码:这决定了哪些层会受到作用力的影响。再次提醒,你可以通过第二章,制作纸杯蛋糕塔来学习如何添加层。因此,在这个变量中,你可以通过使用下拉菜单来指定其中一个。
让我们详细看看每一个,以便更好地理解它们是如何工作的。
二维恒力
这不是一个合适的作用力,因为它不是在区域或区域内起作用,而是直接作用于刚体。因此,它需要连接到具有刚体组件的同一游戏对象。
在检查器中,它看起来是这样的:

注意
出于好奇,对于这个组件,有一个 3D 对应物:恒力。
如其名称所示,它对刚体施加恒定力。通常这用于测试,以便容易地从 检查器 中施加力,但也可以在正常游戏玩法中使用,如果我们需要一个始终受到恒定力控制的物体(这样它就会持续推动)。当然,其值可以通过脚本更改以使其成为非恒定力,但在那种情况下,最好通过脚本中的函数直接施加力。该组件应仅用于恒定力或随时间不改变的力(力可以改变,但长时间内保持恒定)。
此外,此组件还可以用于对刚体施加扭矩。
这里是三个变量的示例:
-
力: 施加到刚体上的恒定力。
-
相对力: 以刚体坐标为基准施加到刚体上的恒定力
-
扭矩: 施加到刚体上的恒定扭矩
这里是一个非测试用例的示例。假设你正在制作一个游戏,其中不同的物体受到不同的重力影响。例如,棕色箱子将像往常一样在地面上跟随,但绿色箱子将向右跟随,因为根据你的游戏设计,它们应该这样做。原因可能是一种实验性血清,用于改变绿色物体的重力。在这种情况下,绿色箱子不能受到正常重力的影响,因此你可以在它们上面放置一个刚体并将 重力缩放 设置为 0。这样,它就不会受到正常重力的影响。然后,放置 恒定 2D 力组件并设置你的重力。此外,如果所有物体都有不同的 恒定 2D 力,你可以在实时更改它们的重力力,也许是在发明另一种血清之后,这种血清改变了左边的重力而不是右边。
面积效应器 2D
此组件定义了一个区域,在该区域内对区域内的所有刚体施加力。你可以配置以特定大小和在该大小上的随机变化的角度的力。你还可以施加线性拖动力和角拖动力来减慢刚体 2D 的速度。
为了工作,此效应器需要一个具有 Used By Effector 和 is Trigger 都设置为 true 的碰撞器。实际上,刚体应该能够进入其中。
该组件有两个折叠菜单(可展开的菜单选项)用于设置相对选项,力 和 阻尼,正如我们从这张图中可以看到的,它显示了 检查器 中的组件:

注意
这两个折叠菜单只出现在 Unity 的最新版本中。所以如果你使用的是旧版本并且没有看到它们,请不要担心;选项只是重新排序了,但它们仍然按此处描述的方式工作。
所有变量都有直观的名称,但让我们快速浏览一下:
-
使用全局角度:如果勾选,则下一个变量将在世界坐标中解释,否则在局部坐标中。
-
力角度:这是要应用力的角度,因此它定义了方向。
-
力大小:这是大小,意味着力的强度。
-
力变化:这是力大小的变化,以便不是始终有恒定的力并提高现实感。请注意不要给出过高值,这可能会导致不希望的行为。
-
力目标:这可以是刚体或碰撞器。在第一种情况下,力将始终作用于质心。在第二种情况下,如果碰撞器不在质心上,力也会为刚体产生扭矩。
-
阻力:这是在区域内部应用线性阻力。
-
角阻力:这是在区域内部应用的角度阻力。
这里是一个使用此组件的例子。假设有一个特殊的设备,能够推动主角的磁性鞋子以对抗重力。因此,在这个设备正上方的区域,您可以放置一个区域效应用户指定向上的力。请注意,如果力的强度小于重力,那么您的角色将会有一个大跳跃,但最终她会掉下来。而如果力值更大,即使没有跳跃,她也会开始向上漂浮,直到遇到障碍物或区域效应用户指定结束。
Buoyancy Effector 2D
此组件用于模拟流体,因此它可以使刚体漂浮。它需要一个同时设置了由效应用户使用和是触发器的碰撞器。实际上,刚体应该能够进入它。
它在检查器中看起来如下:

注意
从这张截图我们可以看到,有两个折叠部分有助于组件的逻辑顺序。如果它们不在那里,请不要担心。您可能在使用 Unity 的较旧版本。
主要变量如下:
-
密度:这表示流体的密度,并且它会影响不同刚体根据刚体密度受到效应对象的影响。实际上,密度较高的刚体会下沉,密度较低的刚体会漂浮,而与流体密度相同的刚体将在流体中悬浮。
-
表面水*:这表示流体表面相对于附加效应对象的 Transform(位置)的位置。零值表示表面将位于物体的中心,并且它位于碰撞器的中间(仅当碰撞器没有偏移时)。您可以从场景视图中的蓝色线条中识别它。
-
阻力:这是在表面下方区域内部应用的线性阻力。
-
角阻力:这是在表面下方区域应用的角度阻力。
-
流动角度:这指定了力场流动力应用的世界坐标角度。因此,它定义了力的方向。
-
流动大小:这指定了流动力的强度,因此力场的浮力水*。它可以取负值,在这种情况下,就像流动角度旋转了 180 度一样。
-
流动变化:这表示流动大小可以变化多少,以便不是恒定的,从而实现更高的现实感。
在场景视图中,组件看起来是这样的,以实际显示表面层级:

此外,这里是其使用的一个示例。假设在你的 2D 游戏中有一个海洋,主要角色必须跳到港口的一些*台上。将此力场放置在海洋水*将使其行为更加逼真。例如,主要角色可以将一个木箱推入海中,使其浮起,然后她可以跳到浮动的箱子上以穿越。

Buoyancy Effector 2D 的一个使用示例,它可以模拟海洋并使木箱浮起;因此,玩家可以跳到木箱上并穿越海洋
点力场 2D
你可以将这个组件想象成一个刚体的磁铁,因此它可以推动/排斥或吸引它们,就像力场的名字所暗示的那样,集中在一个点上。这个点可以通过碰撞器(最常用)或附加到力场相同游戏对象的刚体来确定。
组件需要一个具有由力场使用和是触发器设置为 true 的碰撞器。实际上,刚体应该能够进入其中。
它在检查器中看起来是这样的:

注意
再次强调,展开图只存在于 Unity 的最*版本中。
主要变量如下:
-
力的大小:力的大小,即力的强度。
-
力变化:力的强度变化,以便不是始终有恒定的力并提高现实感。注意不要给出过高值,这可能会导致不希望的行为。
-
距离缩放:当计算刚体和吸引或排斥点之间的距离时,它会被距离缩放缩放(乘以)。这样,你可以修改力场的行为(参见此列表中的力模式)。
-
力源:这可以是刚体或碰撞器。在第一种情况下,吸引或排斥的点将放置在刚体的中心(这意味着在其质心)。在第二种情况下,它将放置在碰撞器的中间。
-
力目标:这可以是刚体或碰撞器。在第一种情况下,力将始终应用于进入效应器的刚体的质心。在第二种情况下,如果碰撞器没有在质心中心,力也会为刚体产生扭矩。
-
力模式:这指定了力的计算方式,它可以是三种类型之一。恒定是最直观的。力始终是恒定的,无论受到效应器影响的刚体与排斥或吸引点之间的距离如何。这意味着只有刚体和点之间的相对位置被考虑来确定力的方向,该方向位于两者之间。相反,在逆线性中,力根据刚体和点之间的距离改变其强度。距离加倍,力强度减半。最后,逆*方,这在大多数情况下是最物理现实的,考虑了距离的*方。这意味着距离加倍,力强度减为四分之一。
![点效应器 2D]()
不同力模式之间的差异
在这个图中,左边是一个排斥点效应器。下面是一个距离刻度,所有这些都包含在点效应器的范围内。假设在距离半 alpha 处,三个不同的刚体从效应点的位置受到相同的应用力。在 alpha 距离处,恒定模式将保持其全部力应用于刚体。逆线性模式将力减半,因为距离加倍。最后,逆*方模式(这是最物理现实的,因为重力和电磁力都有这种行为)将力强度减为距离一半处的四分之一。
-
阻力:这是在效应器区域内要应用的线性阻力。
-
角阻力:这是在效应器区域内要应用的角阻力。
下面是它用法的一个例子。假设主要角色有一个特殊的戒指,当激活时,会吸引金属(当戒指未激活时,它会吸引主要角色的妻子)。因此,每当玩家激活戒指时,游戏就会在主要角色上激活一个点效应器,并通过碰撞掩码变量,只可以选择金属。
*台效应器 2D
此组件为 2D 游戏提供了一个*台效果。例如,它实现了单向碰撞。所以如果角色从*台下方跳起,他将穿越*台,但当他落在*台上时,*台将产生碰撞,保持角色在*台上方。此外,它可以用来去除侧面摩擦/反弹。

使用这个效应器,玩家可以单向穿越碰撞器,但不能反向穿越。在一些*台游戏中,玩家可以从下面跳到*台上,但不能从上面跳下来。
实际上,这是构建*台游戏最常用的组件。想想像《Braid》这样的游戏,玩家可以从下面的*台上跳到上面的*台上。
该组件需要一个设置为由效应器使用但不是是触发器的碰撞器。实际上,刚体应该能够与之碰撞。
这就是它在检查器中的显示方式:

注意
再次强调,展开项仅存在于 Unity 的最新版本中。此外,旋转偏移是一个最*添加的参数。
在这里,你可以看到这个组件在场景视图中的一个可能实例。

这是在其 Gizmo 激活时在场景视图中显示的效应器。
顶部的较大弧线定义了*台/碰撞器不可穿越的方向,而两侧的两个小弧线定义了*台的一侧。
主要变量如下:
-
旋转偏移:这表示整个*台效应器的角度偏移(以度为单位)。这是最*在 Unity 中添加的,多亏了它,可以旋转*台效应器,使其*台倾斜,或者墙壁可以单向穿越。例如,你也可以用它来创建单向穿越的魔法传送门。
-
使用单向:如果勾选,*台将只在单向发生碰撞。
-
使用单向分组:这确保了所有由单向行为禁用的接触都会作用于所有碰撞器。当在穿过*台的对象上使用多个碰撞器,并且它们都需要作为一个组一起作用时,这很有用。
-
表面弧线:这指定了在顶部进行碰撞的弧线的宽度(以度为单位)。在其他所有方向上,如果使用单向被启用,则不允许任何碰撞,允许任何刚体通过,例如你的角色。
-
使用侧面摩擦:如果为真,则对*台效应器的侧面应用摩擦。
-
使用侧面弹跳:如果为真,则对*台效应器的侧面应用弹跳。
-
侧面弧线:这指定了*台效应器两侧(如果旋转偏移设置为
0;否则可能表示上下)的度数,这些被认为是*台效应器的一侧。因此,如果使用侧面摩擦或使用侧面弹跳被启用,这将应用于此处指定的弧线上。
除了使用这个组件创建*台游戏的传统用途之外,让我们看看另一个可能需要使用它的例子。想象一个解谜游戏,其中一些传送门只能单向穿越。因此,我们可以放置一个*台效应器,并设置其旋转偏移,以便只有传送门的一侧发生碰撞。结果,角色只能单向穿越魔法传送门,而不能像以下图中所示的反方向穿越:

这就是如何以另一种方式使用这个效应器,而不是传统的*台方式的一个例子。通过旋转组件的旋转偏移,可以制作一个单向可穿越但另一方向不可穿越的魔法传送门。这可能是一个解谜游戏中的有趣游戏机制。
表面效应器 2D
此组件将切向力应用到由与该效应器关联的碰撞器指定的表面上。换句话说,您可以将其想象成一个传送带,它将刚体沿着力的方向运输,只要它们接触表面。
这就是它在检查器中的样子:

您可以通过两个折叠菜单访问表面效应器 2D的选项,并且您可以找到这里可以设置的参数:
-
速度:切向力应保持的速度。换句话说,它是传送带的速度。
-
速度变化:速度的最大变化,以便不是始终有恒定的速度,尤其是如果您有很多这样的效应器。随机变化范围从零到速度变化中的值。因此,正值带来随机的增量,而负值带来随机的减量。
-
速度缩放:这允许您缩放当效应器尝试将接触的刚体加速到沿表面的指定速度时施加的切向力。如果设置为
0,则实际上不会应用任何力,这就像组件被禁用一样。另一方面,如果设置为1,则表示施加了全部力量。考虑这个参数的一个有用方法是考虑与表面接触的刚体加速到指定速度的速度有多快;较低的值意味着需要更多的时间,而较高的值意味着可以更快地达到速度。然而,您应该小心使用全部力量,因为刚体可能会遇到其他力,导致刚体产生不希望的运动。 -
使用接触力:如果为真,将使用接触力。尽管这更符合物理现实,但它可能会对接触表面的刚体施加扭矩。因此,在现实感取决于设计的视频游戏中,你可以选择启用它,因为默认是禁用的。一个简单的想象方法是,你正在一个真正的传送带上跳跃。传送带的接触力和切向力会使你的腿向前移动,但由于惯性,你的胸部会保持在后面,这可能会导致你跌倒。这意味着你的身体已经开始因为扭矩而旋转,如下面的图所示:
![表面感应器 2D]()
在这个例子中,当你跳上传送带时,你的腿会被传送带的切向力向前推。而你的胸部由于惯性而保持在后面,导致对你的身体施加了扭矩,因此你会跌倒。在 Unity 中,可以禁用接触力,以确保进入与传送带接触的刚体不会受到扭矩。
-
使用摩擦力:如果为真,表面将有摩擦力
-
使用弹力:如果为真,表面将有弹力
当然,自然的使用例子是在你的游戏中使用传送带。但让我们尝试找到另一个例子。假设你的游戏的主角有一个特殊的手套,当它接触金属墙时,能够通过产生磁场来对抗重力。你可以垂直放置一个表面感应器,并向上施加力,直到玩家触摸到那堵墙。
物理材料 2D
Unity 还提供了创建物理材料的功能,以便在物理对象与另一个对象碰撞时调整其摩擦力和弹性。在 2D 情况下,这是通过 Physics Material 2D 实现的。
您可以通过从顶部菜单选择 Assets | Create | Physics Material 2D 来创建一个 Physics Material 2D,如下面的截图所示:

一旦在项目面板中选中,我们可以在 Inspector 中调整其两个值,如下所示:

值得注意的是,我们在本章中看到了许多变量来分配一个 Physics Material 2D,并且它们被组织在一个简单的层次结构中:
-
如果一个 Collider 有 Physics Material 2D,则它具有优先级,并将设置为 Collider。
-
如果一个 Collider 没有分配 Physics Material 2D,则它将分配 Rigidbody2D 中的那个。
-
即使没有 Rigidbody2D 的 Physics Material 2D,也会分配默认的 Physics Material 2D。默认的 Physics Material 2D 可以在 Physics Settings 中设置。
处理 Unity 中的物理
到目前为止,一切都很顺利,我们学习了 Unity 物理引擎的所有单个组件。然而,当涉及到构建自己的游戏时,处理物理可能有点棘手。实际上,只要在一些力上输入一些错误的值,整个场景就会很快变得混乱。解决这个问题的最佳方法是试错法。你实验得越多,你对物理引擎就越熟悉,你将发展出对如何在游戏中*衡所有值的直觉。结果,你将能够使你的场景按照游戏设计文档(或几乎)所描述的那样行动。
处理物理不仅仅是放置不同的组件,还包括如何编程它们。我们已经看到了一些有用的函数,可以应用于刚体以及当两个碰撞体相撞时的一些事件。但还有更多。首先,重要的是要理解,我们看到的所有组件的变量(如刚体、碰撞体、关节和效应器)都可以在运行时通过脚本动态分配。你只需要获取该组件的引用,然后你就可以更改其内部参数。
另一件可能很有用的事情是,可以通过一些函数查询物理引擎,以收集有关周围环境的信息。这些是Physics2D类的静态函数,因此可以使用以下代码片段来调用:
Physics2D.NameOfTheFunction();
当然,你需要将NameOfTheFunction替换为函数。Physics2D类公开了许多这些函数,但让我们只探索主要的一些:
-
OverlapCircleAll(Vector2 point, float radius, [*可选参数]*): 这个函数返回所有在由radius和point(圆的中心)变量指定的圆内的Collider2D数组。换句话说,它检测所有在指定圆内的碰撞体。从碰撞体中,还可以检索到游戏对象本身。我们的纸杯蛋糕塔将使用这个函数来检测周围有多少敌人。此外,该函数的其他可选参数还可以用来指定要搜索的层掩码以及深度(z轴)的最小和最大值。 -
OverlapCircle(Vector2 point, float radius, [*可选参数]*): 与之前的函数相同,但它不是返回完整的数组,而是返回第一次出现的结果。这在只需要检测某个物体是否在圆内时非常有用。 -
RaycastAll(Vector2 origin, Vector2 direction, [*可选参数]*): 从origin向direction射出射线,并返回射线在RayCastHit2D数组(见后文)中击中的所有不同碰撞体,这是一个指定击中详细信息的类,包括碰撞体。此函数在您需要验证空间中是否存在某个物体时非常有用。此外,其他可选参数可以指定射线可以达到的最大距离、层掩码以及深度(z 轴)的最小和最大值。 -
Raycast(Vector2 origin, Vector2 direction, [*可选参数]*): 与上一个函数相同,但不是返回完整数组,而是只返回第一次击中的结果。
注意
您可以在官方文档中找到 Physics2D 类的所有功能:docs.unity3d.com/ScriptReference/Physics2D.html。
关于 RayCastHit2D 类,以下是我们可以检索的信息列表:
-
centroid: 执行投射所使用的原初体的质心 -
collider: 被射线击中的碰撞体 -
distance: 射线原点到击中点的距离 -
fraction: 击中发生沿射线的距离分数 -
normal: 被射线击中的表面的法向量 -
point: 射线在全局空间中击中碰撞体表面的点 -
rigidbody: 附着在击中对象的Rigidbody2D -
transform: 被击中对象的变换
总之,查询物理引擎是收集信息的一种常见做法,我们将在我们的 塔防 游戏中这样做。
物理学的其他事项
与其他章节一样,这是一个可选部分,其中包含对章节中主题的更深入见解。所以,如果您不感兴趣,请随意跳过本节,直接进入下一节。否则,就再喝点咖啡,继续阅读。
碰撞体上的 Simulate 设置
本节的目标是解释在刚体上启用和禁用物理组件,以及在刚体组件上启用和禁用 Simulate 设置之间的区别。
每次物理组件被添加、启用、删除或禁用时,物理引擎的内部内存都会更新(分别将组件添加到或从内存中删除)。当 Simulate 设置被禁用时,物理引擎只是停止对其执行计算——它不会从内存中删除对象。因此,当 Simulate 再次被选中时,物理引擎已经将所有对象/组件放入内存中,它不需要从头创建它们,从而提高了性能。
当然,如果你需要永久地从场景中移除刚体,那么只需删除组件即可,因为如果你只是取消选择模拟,组件仍然会在内存中,导致内存管理不佳。
2D 物理射线投射器组件
回到第三章,“与玩家通信——用户界面”,我们看到了 Unity UI 系统,在可选部分有画布的不同组件。其中之一是图形射线投射器,它能够检测屏幕上的用户输入。此组件检查玩家是否真的滑过了滑块或点击了按钮,然后通过与事件系统交换消息来触发事件。
如果我们有物理对象并且想要以类似于图形射线投射器为 UI 做的方式交换它们的事件,我们可以在相机上使用一个物理射线投射器 2D 组件来处理这些事件。
一旦添加了此组件,你就可以在物理对象的脚本中实现不同的接口。结果,当触发相应的事件时,它们将自动调用所实现的函数。
例如,一个事件可能是某个关节断裂,当这种情况发生时,你可能想要运行一些代码。此外,一些信息将被提供给函数;在关节的情况下,断裂关节的力的大小作为参数传递。
其他物理设置
在这里你可以找到其他物理设置:
-
速度迭代次数:在更新期间确定物理体速度所进行的迭代次数。数字越高,模拟越精确。缺点是计算成本。默认值是
8。 -
位置迭代次数:在更新期间确定物理体位置所进行的迭代次数。数字越高,模拟越精确。缺点是计算成本。默认值是
3。 -
速度阈值:与相对速度低于此值的碰撞被视为非弹性碰撞,这意味着碰撞的物体不会相互弹开。
-
最大线性校正:在解决约束时使用的最大线性位置校正。它可以是
0.0001到1000000之间的任何值。它有助于防止超调。 -
最大角校正:在解决约束时使用的最大角校正。它可以是
0.0001到1000000之间的任何值。它有助于防止超调。 -
最大*移速度:这是你的游戏中一个物体可能具有的最大(*移)速度。这个值是上限,这意味着任何试图达到更快速度的对象都将被限制在这个值。
-
最大旋转速度:这是你的游戏中一个物体可能具有的最大(旋转)速度。与之前相同的推理适用,只是用旋转代替*移。
-
最小穿透量以应用罚力:在应用任何分离冲量力之前允许的最小接触穿透半径。
-
Baumgarte 比例尺:这是一个比例因子,用于确定碰撞重叠解决的速度(见信息框)。
-
Baumgarte 碰撞时间比例尺:一个比例因子,用于确定碰撞时间重叠解决的速度(见信息框)。
注意
Baumgarte 的约束稳定化方法(有时简称为 Baumgarte 方法)是由 J. Baumgarte 在 1972 年发明的一种用于解决某些碰撞约束的算法,例如与关节。它足够快,可以在实时应用中使用,如视频游戏或机器人技术。这个技巧在于利用一些导出的微分方程的解析形式,这些形式是数值解决的。这使得算法不仅比其前辈运行得更快,而且精度更高。
Baumgarte 比例尺是算法的重要参数,它表示要应用的校正比率。一个常见的值,通常作为默认值给出,是 0.2,也是 Unity 的默认值。
值越高,你的关节越容易失控。另一方面,值越低,你的关节动作越少,可能会导致弹簧感。
-
查询击中触发器:这是一个切换按钮,如果设置为真,允许射线投射物击中触发体积。默认情况下,它是真的,但如果不想射线投射物击中触发体积,只想击中碰撞器,则可以取消选中。何时取消选中此框实际上取决于你游戏的设计,以及你打算如何编程它。其他物理设置
当“查询击中触发器”设置为假(图的上半部分)时,触发体积不会被射线投射检测到。相反,当“查询击中触发器”设置为真(图的下半部分)时,触发体积也会被射线投射检测到,并作为碰撞返回。
-
碰撞器内查询开始:这是一个切换按钮,如果设置为真,允许从碰撞器内开始的射线投射物检测到碰撞器。默认情况下,它是真的,但如果许多射线投射物在碰撞器内开始并且你不想它们被返回为碰撞,则可以取消选中。再次强调,何时取消选中此框实际上取决于你游戏的设计,以及你打算如何编程它。其他物理设置
当“查询开始于碰撞器”设置为假(图的上半部分)且射线投射物的来源在碰撞器内时,这个碰撞不会返回。相反,当“查询开始于碰撞器”设置为真(图的下半部分)时,射线投射物的来源所在的碰撞器也会被返回为碰撞。
-
停止播放更改:这是一个切换按钮,如果设置为真,则如果任何参与碰撞的 GameObject 被删除或移动,则立即停止报告碰撞回调。默认情况下,它是假的。
-
Gizmos: (本折叠图的描述将在下一节中介绍。)
碰撞体的 Gizmos
本节描述了物理 2D设置中之前菜单项Gizmos。
Gizmos是一个折叠图,显示你在编辑器内关于碰撞体可视化的额外选项。这些选项在调试中非常有用。这是它在检查器中的外观:

这里是对显示的选项及其用法的解释:
-
Always Show Colliders: 默认情况下,你只能在游戏对象(或其子对象之一)包含此类碰撞体时才能看到碰撞体。如果你启用此选项,你将始终能够看到碰撞体(无论何时Gizmos可见)。
-
Show Collider Sleep: 当启用时,它允许你在物理引擎中的睡眠模式下看到碰撞体。
-
Collider Awake Color: 这指定了当显示时唤醒(非睡眠)碰撞体应具有的颜色。默认情况下,它是一种浅绿色,其 alpha 通道(不透明度)设置为
192。 -
Collider Sleep Color: 这指定了当显示时睡眠碰撞体应具有的颜色。默认情况下,它与碰撞体唤醒时的相同浅绿色,但 alpha 通道(不透明度)设置为
92。 -
Show Collider Contacts: 当启用时,这允许你在碰撞体碰撞时看到接触点。它们显示为箭头(如下一图所示)。
-
Contact Arrow Scale: 此值允许你缩小由碰撞体的接触点显示的箭头。默认情况下,其值为
0.2(如下一图所示)。 -
Collider Contact Color: 这指定了表示碰撞体接触点的此类箭头的颜色。默认情况下,它设置为浅紫色(如下一图所示):
![碰撞体的 Gizmos]()
左边有两个相互重叠的碰撞体。在中间,物理引擎更新碰撞体以模拟碰撞(因为它们不能重叠)。同时显示两个箭头之间的接触点。在右侧,是中间的相同图,但Contact Arrow Scale设置为
0.6而不是0.2,因此箭头更大。 -
Show Collider AABB: 当启用时,这允许你看到碰撞体的轴对齐边界框(AABB)。正如其名称所暗示的,它是一个完全包含碰撞体的盒子,并且它与世界坐标系的轴对齐。例如,多边形碰撞体的边界框如下(在左侧):
![碰撞体的 Gizmos]()
左边是具有其 AABB 的多边形碰撞体;右边是前一个图的相同图,但显示了其 AABB。
- Collider AABB Color: 这指定了当显示时碰撞体 AABB 的颜色。
我们游戏中的物理
在本节中,我们将将本章的一些概念应用到我们的游戏中。特别是,我们将看到如何使用物理引擎检测当喷雾击中熊猫并对其造成伤害的情况。
设置 Pandas 为刚体
由于我们将利用物理引擎,我们需要正确设置熊猫,使其在场景中成为一个物理对象。这意味着给它一个刚体组件。
因此,我们可以先给 Panda 预制件添加一个 Rigidbody2D 组件,并将其 Body Type 设置为 Kinematic,如下截图所示:

理论上,我们应该已经完成了,因为熊猫现在被视为一个物理对象。然而,在上一章中,我们编写了一个函数,允许熊猫通过直接在它的 Transform 上分配新位置来移动。由于现在熊猫有一个 Rigidbody2D 组件,我们不能再这样做(就像我们之前在 刚体 部分中解释的那样)。因此,我们需要稍微修改 PandaScript。特别是,我们需要获取熊猫刚体的引用,然后使用 MovePosition() 函数为 Kinematic 刚体。基本上,我们正在应用我们在 处理刚体 部分中学到的知识。
因此,打开脚本,并添加以下私有变量:
*//Private variable to store the rigidbody2D*
private Rigidbody2D rb2D;
然后,在 Start() 函数的末尾添加以下行:
*//Get the reference to the Rigidbody2d*
rb2D = GetComponent<Rigidbody2D>();
在 MoveTowards() 函数中,我们需要在熊猫的刚体上使用 MovePosition() 函数来改变其位置。此外,我们不再使用 deltaTime,而是用 fixedDeltaTime 替换它。因此,这里突出显示了与上一章相比的变化:
*//Function that based on the speed of the Panda makes it moving towards the destination point, specified as Vector3*
private void MoveTowards(Vector3 destination) {
*//Create a step and then move in towards destination of one step*
float step = speed * Time.fixedDeltaTime;
rb2D.MovePosition(Vector3.MoveTowards(transform.position,
destination, step)); }
注意
我们需要记住,现在应该调用 MoveTowards() 函数是在 FixedUpdate() 中,而不是在 Update() 中。我们将在 第七章 中看到这一点,“交易纸杯蛋糕和终极蛋糕争夺战 – 游戏玩法编程”。但下一个部分可以有一个例子。
最后,我们可以保存脚本,我们就完成了熊猫的设置。
设置弹射体为刚体
与我们为 Pandas 做的类似,我们需要给所有弹射体添加一个 Rigidbody2D 组件,并将 Body Type 再次设置为 Kinematic。
如果你还记得,弹射体过去是通过直接改变它们的 Transforms 来移动的,我们需要修复这个问题,因为它们也有一个 Rigidbody2D 组件。
打开脚本,就像我们为 Pandas 做的那样,添加以下私有变量:
*//Private variable to store the rigidbody2D*
private Rigidbody2D rb2D;
然后,在 Start() 函数中,让我们获取它的引用:
*//Get the reference to the Rigidbody2d*
rb2D = GetComponent<Rigidbody2D>();
现在,我们需要将 Update() 函数替换为 FixedUpdate() 函数,因为我们正在处理物理引擎。此外,我们还需要稍微修改一下代码(注意也使用了 fixedDeltaTime):
*// Update the position of the projectile according to time and speed*
void FixedUpdate() {
rb2D.MovePosition(transform.position + direction *
Time.fixedDeltaTime * speed);
}
注意
一个细心的读者会注意到,我们正在应用“处理刚体”部分信息框中解释的运动方程。特别是,这里我们将速度分解为方向和速度(将它们相乘后得到速度)。
保存脚本,接下来让我们看看如何在下一节中检测到水滴击中熊猫。
检测水滴
为了检测水滴与熊猫之间的碰撞,我们需要给它们都添加一个Collider2D。你可以选择最适合你需求的,我会选择Box2D碰撞器。然后,你需要让其中一个对象充当触发器,在我们的例子中,我们可以选择熊猫。
下一步是实现OnTriggerEnter2D()函数,该函数由 Unity 的物理引擎在PandaScript中调用。结果,我们能够检测到有什么东西击中熊猫,并检查它是否是实际的水滴,以便使用上一章中编写的Hit()函数对熊猫造成伤害。
*//Function that detects projectiles*
void OnTriggerEnter2D(Collider2D other) {
*//Check if the other object is a projectile*
if(other.tag == "Projectile") {
* //Apply damage to this panda with the Hit function*
Hit(other.GetComponent<ProjectileScript>().damage);
}
}
注意
当然,我们需要确保带有“Projectile”标签的每个对象都附加了ProjectileScript组件。这个检查留作练习。
最后,保存脚本,我们至少在这一章中完成了游戏中的物理部分。实际上,在下一章中,我们还会再次使用物理引擎,但出于其他原因。
作业
正如每一章一样,这里有一些练习可以帮助你练习你的技能:
-
被遗忘的刚体:有时在检查器中的组件可能会被遗忘。然而,我们可以通过在代码中创建警告来简化这个过程。对于我们的熊猫和水滴,在其脚本中创建一个检查,当游戏对象初始化时,如果缺少Rigidbody2D组件,则添加它,并将其设置为Kinematic。此外,你可以打印一条警告信息(参见第八章,“蛋糕之外是什么?”,了解更多关于调试信息)。
-
对加速度的热情:在第八章中,我们看到了如何为我们的Kinematic刚体实现运动方程。特别是,本章展示了一个速度方程的实现。现在,尝试为Kinematic刚体实现加速度方程。
-
关节大师:对于 Unity 提供的每一个关节,思考一个可能的用途和示例(可能不同于章节中已经展示的)。然后,在纸上绘制物理系统,确定哪些是刚体以及锚点在哪里。最后,在 Unity 中重现你所想象的内容,并调整所有设置,直到它按你决定的方式工作。
-
《效果器大师》:对于 Unity 提供的每个效果器,思考可能的用途和示例(可能不同于章节中已经展示的)。然后,在纸上绘制效果器应该如何工作的草图,并确定不同的刚体如何与之交互。最后,在 Unity 中重现你的想象,并调整所有设置,直到它按你决定的方式工作。
-
《不那么胆怯的大熊猫(第三部分)》:如果你也完成了第四章中的这个练习的第二部分,不再孤单——甜食爱好者的大熊猫出击,一些大熊猫会被击晕,而另一些则不会,这取决于布尔值。不要在检查器中暴露这个布尔值,而是将这个属性添加到
Projectile类中,这样大熊猫是否被击晕就取决于它被哪种投射物击中(我们将在第六章,穿过糖果雨——人工智能中的导航中看到这一点)。 -
《不那么胆怯的大熊猫(第四部分)》:在你完成了这个练习的第三部分之后,如果可怜的大熊猫被太多的糖果雨攻击,它可能不会再移动,因为它总是处于击晕状态。结果,它也无法避开糖果雨。为了避免这种情况,我们需要修改大熊猫脚本,使得如果另一个糖果雨在大熊猫被击晕时击中它,它会从大熊猫那里扣除健康值;但不会从开始就触发击中动画和/或击晕阶段。
-
《不那么胆怯的大熊猫(第五部分)》:既然你已经完成了这个练习的第三部分,让我们改进整个击晕大熊猫的系统。给每个投射物添加一个变量,表示这个投射物击晕大熊猫的概率。最后,根据前面的变量以概率触发大熊猫的击晕阶段。
最后,如果你喜欢挑战,这里有一个挑战给你:
-
《冻结的传送带(第一部分)》:想象一条由一大块冰制成的传送带,它滚动得相当快。思考并描述当一个箱子掉到上面时会发生什么。记住,冰上的摩擦力非常低,需要考虑惯性。如果掉下去的不是箱子,而是一个球体呢?解决方案在书的末尾。
-
《冻结的传送带(第二部分)》:一旦你完成了第一部分,就在 Unity 的物理引擎中重现冻结的传送带,并用箱子和球体进行测试。
摘要
在本章中,我们首先学习了物理学的一些基本概念,以便更好地开发我们的游戏。然后,我们了解了 Unity 的物理引擎,它分为几个组件。刚体和碰撞体描述了游戏中物理对象的特点,而关节和效果器则影响它们在环境中的相互作用方式。
最后,我们学习了如何处理物理问题,并从我们的 塔防 游戏中提取了所需内容,以便实现洒水器和熊猫之间的碰撞(并调用正确的函数来更新熊猫的健康状态和动画)。
第六章. 穿越糖霜之海 – 人工智能中的导航
在为我们的熊猫赋予渲染(第四章,不再孤单 – 甜食熊猫出击)和物理形状(第五章,秘密成分是物理学的一点点)之后,现在是时候赋予它们智能了。特别是,让它们能够通过地图走向玩家的蛋糕并吃掉它的能力。实际上,正如我们已经指出的,人工智能(AI)是赋予 NPC 生命力的核心,使它们能够在世界中移动和行动。然而,本章将专注于导航。
特别是,我们将为我们的熊猫实现一个航点系统。我们将做两次,这样我们可以从不同的角度看待同一件事,并突出每种方法的优缺点。
下面是我们将要讨论的主题概览:
-
人工智能在视频游戏中的重要性
-
视频游戏中的导航及主要技术概述
-
将航点系统作为静态列表实现
-
将航点系统作为游戏对象的动态池实现(并在地图上显示为图标)
-
了解导航之外的领域
正如本书的所有其他章节一样,你可以在作业部分练习你的技能。所以,让我们准备开始吧!
准备工作
本章的唯一要求是,你已经完成了书中所有关于PandaScript的部分。
人工智能简介
人工智能(AI)是一个广泛的话题,即使我们只限制在视频游戏领域。实际上,由于其复杂性,它是视频游戏编程中最难的部分之一。一个优秀的 AI 程序员应该具备数学(如图论、贝叶斯网络、运筹学等)、物理(如运动方程)和心理(以理解玩家对游戏中 AI 的反应)的知识。前两者是众所周知的,而最后一个有时会被忽视,但同样重要。事实上,有时 NPC 角色的最可信行为并不是玩家最享受的。如果你对了解更多感兴趣,我写了一篇文章,你可以在我的网站上找到:francescosapio.com
然而,我不希望因为我引用了这样的数学概念而吓到你。实际上,在这些部分,我们不会涉及这样复杂的事情,但了解我们将要做什么的基础是有用的,即使我们只是创建一个简单的地面来使我们的塔防游戏工作。无论如何,我邀请你更多地了解视频游戏中的人工智能,因为你可以实现的事情真的很棒!
人工智能在视频游戏中的重要性
想象一下一款没有敌人可以竞争的塔防游戏,只有你和一堆塔。像 SimCity (www.simcity.com) 这样的游戏将不复存在。游戏将变成可预测的体验,它们的重玩价值将大大降低,MMO 游戏将变得普通。这将是一场动态游戏玩法的大灾难,尽管不至于太过戏剧化。因此,为了使任何游戏提供动态和不断增长的经验,AI 是必不可少的。
人工智能,简称 AI,允许系统像人类或动物一样思考和行动。随着时间的推移,这些系统可以从其用户的行为中学习;例如,如果它们进展得太容易,或者如果它们在挣扎,那么系统就能够调整游戏(实时)以使游戏适应玩家。这个概念指的是机器学习。
计算机游戏中的 AI 指的是游戏组件(如非玩家角色,简称 NPC)的行为和决策过程。在现代游戏中,存在实时、非常动态的 AI,在某些情况下感觉就像你在与其他真实玩家对战。这样,做得好的 AI 允许你快速、明智地做出决定,以便在游戏中取得进步和成就。游戏中 AI 的例子可以从早期的街机游戏如 Pac-Man,到第一人称射击游戏中的敌人,如 Battlefield、Call of Duty 和 Alpha Protocol;或者策略游戏中的 魔兽世界 和 剑网 3 中的成群结队的兽人怪物和野兽。
在一本关于人工智能的书(《游戏人工智能》由 Ian Millington 和 John Funge 所著),我推荐你看看,我们可以将 AI 视为具有以下状态:
-
移动:这指的是涉及 NPC 做出决定,然后产生某种运动(如攻击或逃离玩家)的 AI。
-
决策:正如其名所示,这要求 NPC 做出决定,决定下一步要做什么。例如,如果敌人看到你,它会攻击、逃跑还是呼救?
-
策略:想象一下试图协调一个整个团队,比如在 S.W.A.T 或汤姆·克兰西的 Rainbow Six 中。在这些情况下,AI 不仅影响一个或两个角色,还影响整个团队,而团队本身可能有自己的决策树,比如在看到敌人时该做什么;是自己解决他们,还是通知你?
-
基础设施:这指的是 AI 的结构方式,这最终将决定它在游戏中的表现效果。这不仅仅是关于创建正确的算法来使 NPC 执行某些动作,还涉及到以高效的方式利用计算机资源。
-
基于代理的 AI:这指的是创建自主的非玩家角色(NPC),它们从游戏数据中获取信息,确定要采取的行动,然后执行这些行动的概念。
小贴士
一个值得查看的网站是英特尔网站:tinyurl.com/IntelAI,它提供了关于在游戏中使用人工智能的精彩解释和介绍。
导航
现在,我们应该更好地理解为什么人工智能对视频游戏如此重要和关键,但这是一个如此广泛的话题,无法在这个小章节中处理。因此,我们将专注于一个特定的方面,即导航。由于这是一个关于该主题的入门章节,我们希望理解导航的基本概念,但只实现我们将要在游戏中使用的一种简单技术。
导航方面
游戏角色在游戏及其关卡内移动。移动可以非常简单,例如街机游戏或 NPC 跟随或瞄准你,而其他角色则可以非常复杂,就像在快节奏的动作和冒险游戏中。在游戏中实现固定路线很简单,但请注意,当物体和其他角色挡道时,可能会打破它们的幻觉。例如,在 NPC 高度密集的游戏中(如刺客信条),在环境中漫游的角色可能会卡在环境物体上,看起来像是在月球行走,或者说是在移动但并没有去任何地方。在更动态的情况下,跟随你或向你走来(朋友或敌人)的角色将不知道你的未来移动,因此必须相应地行动,就像你一样。这可以从实时策略游戏中的敌人波到你需要避开以渗透高级安全建筑物的守卫。
对于这些角色(和情况)中的每一个,人工智能必须能够在游戏关卡内计算出一条合适的路径,确保它能够对进入其路径的物体做出反应,以达到其目标。理想情况下,你希望角色尽可能自然地行动。
注意
在更大的背景下,导航可以用于一个空间,它可以代表环境,也可以代表更抽象的事物,如问题的移动空间。例如,在著名的八皇后游戏中(在棋盘上放置八个皇后,使得它们中的每一个都不会攻击另一个;你应该试一试),在移动空间中找到路径可能等同于找到解决方案。
在视频游戏中,导航可以以各种形式存在,例如:
-
转向行为:这些行为为智能体找到一条避免碰撞的即时路径。它可以用于基本的障碍物避让,也可以在多智能体系统环境中使用。由于这些行为处于低级别,在视频游戏中,它们一直是以运动学方式实现的(就像我们在第五章,物理学的小调料中看到的那样)。然而,最*,游戏开发世界中出现了某些动态转向行为,使得游戏更加逼真。
-
路径查找:这种查找从起始位置到目的地找到一条路径。这个级别是最常用的,已经发现/发明并实现了许多技术。本章将重点关注这种类型的导航。
-
驱动路径查找:这种查找根据某些驱动行为找到一条或多条路径。这在游戏行业中从未被实现,但在学术界是游戏开发的研究领域,因此值得提及。这个级别位于决策和路径查找之间。实际上,在路径查找时做出一些决策,这带来了更智能的路径查找和决策过程中的效率。
注意
更多关于不同类型导航的信息可以在我的网站上找到更详细的介绍。此外,我的研究涉及直接驱动的路径查找,例如 BDP(我们将在本章后面看到)。以下是链接:francescosapio.com。
路径查找及其技术
在过去的几十年里,已经探索了许多路径查找算法和技术。最早发明的路径查找算法之一是Dijkstra 算法,它为现代路径查找算法奠定了基础。当然,自从 Dijkstra 以来,取得了很大的进步,算法变得更加高效(尤其是在我们处理特定信息或我们有关于问题的先验知识可以使用时)。在视频游戏中最常用的是A*算法(及其所有衍生算法),它使用一些关于地图的额外信息。Dijkstra 的主要概念是在所有方向上探索,直到找到一条路线;A*的主要概念是朝着目的地方向探索(这听起来可能很简单,但并不总是容易确定一个函数,以某种方式告诉你朝向目的地的方向)。当然,这是一个简化,但已经足够获得更好的概述。
上述提到的算法在许多情况下都有效,但如果我们要找到的路线足够简单,可以在一个小地图上找到,那么开发这样的算法就不再值得,因为还有更简单的技术。其中一种技术是使用航路点,这是我们游戏将使用的技术。主要概念是将地图分成一个图,这个图可能非常小(足够用手绘),路径查找可以以分布式的方式进行。当然,分布式路径查找之外还有另一个广阔的世界(一个实际应用是需要在互联网上穿越世界的 IP 数据包,不同路由器之间的路径是以分布式方式确定的,因为网络地图不断变化)。
另一个影响寻路(但也是 AI 的其他技术)的重要因素是算法是否需要在线工作或离线工作。在线意味着算法需要实时找到解决方案,而离线意味着解决方案可以在事先找到。在视频游戏中,有些情况下我们需要使用在线解决方案(例如为 NPC 找到路径),而有些情况下我们需要离线解决方案(例如,在回合制游戏中,或者当某些 AI 计算在加载时执行时)。
不幸的是,我们可能需要另一本书来描述视频游戏中的导航,但这一章为你提供了一个美好而温和的介绍,让你可以继续学习游戏中的 AI,并可以使用参考(建议的书籍和链接)继续你的学习之旅。
敌人路径点
路径点是在地图上的一个特殊点,NPCs 会改变方向移动到另一个路径点。它们可以包含逻辑,实际上引导角色移动到随时间变化的具体位置,例如靠*玩家。例如,在射击游戏中,敌人想要靠*玩家射击。路径点还可以执行决策过程的一部分。例如,想象一个塔防关卡,敌人的路径分为两个方向。在这种情况下,路径点可以用来决定特定敌人应该走哪个方向(我们将在本书的最后一章中看到这一点)。路径点的优点是,在某些情况下,它们可能比实现完整的寻路算法更有效率。
注意
在更复杂的实现中,路径点可以通过不同的方式连接,并且这些连接也可以通过让路径点相互寻找来自动创建。此外,它们还可以包含其他信息,例如哪个路径点是玩家最*的。在这里,敌人可以询问或查询路径点,以确定前往玩家的方向,而无需在地图本身上运行完整的寻路算法。
目前,我们不需要在路径点后面实现特定的逻辑。然而,它们是一个有用的工具,因为它们允许我们轻松地在地图上移动敌人,并且它们足够模块化,以便能够创建游戏的其他级别而不会遇到太多问题。
在本节中,我们将学习如何创建路径点。特别是,我们将看到两种实现路径点的方法。
获取路径点坐标
在我们开始创建路径点之前,我们首先需要决定在地图上放置它们的位置。因此,我们需要找到所有我们的 Pandas 改变方向的地方。在这个简单的地图上,它们位于路径的所有角落。在下面的图像中,它们由红色圆点表示:

如我们所见,有 11 个航点,我们需要一个位于地图蛋糕上的航点。这个最终的航点是熊猫成功完成偷吃玩家蛋糕任务的终点。
现在我们已经发现了它们,我们需要记录它们在地图上的坐标。一种快速的方法是在 场景 视图中拖动 Panda Prefab,然后记录航点的位置。在这种情况下,我们可以获得以下数据:
| 航点编号 | X 坐标 | Y 坐标 |
|---|---|---|
| 1 | -28 |
8 |
| 2 | -28 |
-16 |
| 3 | -16 |
-16 |
| 4 | -16 |
7 |
| 5 | -2 |
7 |
| 6 | -2 |
-6 |
| 7 | 12 |
-6 |
| 8 | 12 |
9 |
| 9 | 25 |
9 |
| 10 | 25 |
-17 |
| 11 | 32 |
-17 |
实现航点 – 第一种/静态方法
现在我们已经拥有了所有的航点坐标,我们可以实现它们。在本节中,我们将探讨实现它们的第一种方法。这种方法的主要优势是实现简单,以及有机会了解更多关于静态变量和遍历航点。
在游戏管理器中实现航点
在这个航点的首次实现中,它们在游戏中不会是独立的实体,而是一系列按特定顺序排列的位置。所有敌人都会查阅这个列表,并根据它们当前所在的航点,选择列表中的下一个航点。
当然,这种方法有一些限制,例如我们无法在航点中实现自定义功能,正如我们将在第八章 What Is beyond the Cake?中看到的。然而,它更容易实现,并给我们提供了探索如何使用静态变量的机会。
首先,我们需要创建另一个脚本,并将其命名为 GameManagerScript。我们将在下一章中在这个脚本中实现更多功能。但到目前为止,我们需要存储航点。实际上,目前我们只需要添加一个位置数组,所以你可以在脚本中写下以下内容:
*//public waypoint list as an array of positions*
public Vector3[] waypoints;
它是一个 Vector3 数组,基本上只是按特定顺序存储一组位置。保存代码,并在 场景 中创建一个空的游戏对象,你可以将其重命名为 Game Manager。将脚本附加到它上,在 检查器 中你应该看到如下内容:

我们需要将数组的元素数量设置为找到的航点数量,在本例中为 11。因此,我们的 检查器 看起来如下:

最后,我们可以用航点位置填充所有这些值。那么 z 轴怎么办呢?由于我们不希望 Pandas 改变它们的 z 轴,我们可以将其值设置为与 PandaPrefab 的相同 z 轴值,即 -1。最后,我们应该得到如下所示的内容:

注意
读者可能会想知道使用 Vector2 而不是 Vector3 是否值得。答案是:由你决定。没有任何东西阻止你使用 Vector2 而忽略 Vector3。但是,既然我们已经选择了用 z 缓冲区来处理游戏中的深度,我个人更倾向于直接控制 z 轴,因此请确保实现预期的行为。
沿着设计的路径移动 – 静态
接下来,我们需要给敌人一个访问存储在 GameManagerScript 中的航点的机会。因此,我们需要获取它的引用。有好多方法可以做到这一点,但为了学习的目的,我们将使用一个静态变量(以便揭示此类变量的用途)。实际上,所有的 Pandas 都共享同一个游戏管理器,如果每次创建 Pandas 时都需要搜索 Game Manager,那么这将是计算资源的浪费。静态变量是一个在 PandaScript 的所有实例之间共享的值。当然,我们需要小心不要多次分配这个变量。
注意
记住,静态变量在不同的场景/级别之间是持久的。因此,如果你计划发布一个包含多个级别的游戏,那么在改变级别时,你很可能需要重置这个变量。我们将在第八章 What Is beyond the Cake? 中更好地探讨这一点。
打开 PandaScript,让我们添加一个静态变量来存储对 Game Manager 的引用:
*//Private static variable to store the Game Manager*
private static GameManagerScript gameManager;
在 Start() 函数的开始处,我们需要检查是否已经有一个实例(另一个 Pandas)分配了这个变量。如果没有,我们将通过在场景中找到它的引用来分配它,尽管当时场景中只有一个游戏管理器。因此,这个 Pandas 实际上会初始化这个变量。这样,所有将要创建的其他 Pandas 实例都将有一个对 Game Manager 的引用准备就绪,并且由于这个检查,我们可以确信我们只分配了一次:
*//If the reference to the Game Manager is missing, the script gets it*
if(gameManager == null) {
gameManager = FindObjectOfType<GameManagerScript>();
}
现在,我们必须让 Pandas 移动。但首先,我们需要一个变量来存储 Pandas 正在前往的当前航点:
*//Private counter for the waypoints*
private int currentWaypointNumber;
然后,我们需要一个常量来设置一个阈值,超过这个阈值航点就被认为是已经到达了。实际上,存在数值不稳定性,我们不能直接检查航点的距离是否实际上是零,而只能是一个非常接*零的值。正如你所见,分配给这个常量的值非常低:
*//Private constant under which a waypoint is considered reached*
private const float changeDist = 0.001f;
最后,我们需要实现熊猫向右的航点移动机制,并在到达前一个航点后改变方向前往下一个航点。由于我们将使用的MoveTowards()函数处理物理问题,我们需要在FixedUpdate()函数内实现整个航点机制,正如我们在第五章《物理的调味品》中学到的那样。因此,我们可以开始编写以下代码:
void FixedUpdate() {
*//Add here the rest of the code of this section*
}
特别地,在FixedUpdate()函数内我们需要做三件事情。第一件事是检查熊猫是否到达了航点列表的末尾,这意味着它已经站在美味玩家蛋糕的前面。如果是这样,我们需要以与在第四章《不再孤单——甜食熊猫出击》中触发其他动画相同的方式触发吃动画。然后,我们需要从这个熊猫上移除这个脚本。实际上,我们在第四章《不再孤单——甜食熊猫出击》中编写的状态机行为脚本将负责将熊猫从场景中移除。最后,我们返回,这样函数的其余部分就不会被执行:
*//if the Panda has reached the cake, then it will eat it, by triggering
the right animation,*
*//and remove this script, since the State Machine Behaviour will take
care of removing the Panda*
if (currentWaypointNumber == gameManager.waypoints.Length) {
animator.SetTrigger(AnimEatTriggerHash);
Destroy(this);
return;
}
第二件事,如果熊猫还没有到达最后一个航点,就是计算当前熊猫位置(通过其 Transform)与它正前往的航点之间的距离。这个值存储在一个局部变量dist中:
*//Calculate the distance between the Panda and the waypoint that the
Panda is moving towards*
float dist = Vector2.Distance(transform.position,
gameManager.waypoints[currentWaypointNumber]);
最后一件事情是检查熊猫是否足够接*航点。足够的意思是低于存储在changeDist中的常量阈值。如果是这样,我们就增加航点的计数器,这样在下一个迭代中,熊猫就会前往下一个航点。否则,我们只需使用上一章中实现的MoveTowards()函数将熊猫移动到航点:
*//If the waypoint is considered reached because below the threshold of
the constant changeDist*
*//the counter of waypoints is increased, otherwise the Panda moves
towards the waypoint*
if(dist <= changeDist) {
currentWaypointNumber++;
}else {
MoveTowards(gameManager.waypoints[currentWaypointNumber]);
}
我们可以保存我们的脚本并测试它。通过在场景中某个位置放置一个熊猫,靠*第一个航点,然后按下播放,我们将看到它沿着路径移动。
实现航点——第二种/动态方法
在本节中,我们将探讨实现游戏航点系统的第二种方法。当然,结果将是相同的,但这种方法提供了许多其他优点。首先,对于设计师来说,在地图本身中定位、更改、移动和替换航点更容易。其次,它允许在行为上具有很大的灵活性,可以以这种方式实现,使设计师更容易使用创建的脚本。我们将在本书的最后一章中利用这个系统的某些潜力。
然而,这种方法确实存在一些缺点,就像生活中的每一个选择一样。特别是,系统的复杂性增加了。此外,它为每个航点使用不同的游戏对象,如果航点的数量真的很多,这一点至关重要。
注意
为了克服为每个航点使用不同游戏对象的问题,我们有多种可能性,但让每一个都工作是一个挑战,同时对于设计师来说也很容易使用。实际上,航点仍然可以存储为列表,不是位置,而是waypoint类,同时提供功能,允许设计师在场景视图中编辑和放置它们。这被留在了作业部分作为一个挑战。
将航点作为独立实体实现
到目前为止,我们已经看到了航点的简单实现。现在,我们将再次实现它们,但这次作为独立的实体。因此,在第八章,蛋糕之后是什么?中,我们将探讨如何在游戏中解锁航点的潜力。实际上,在本节结束时,对我们游戏的影响将是相同的;然而,我们将通过实现更多功能来改变第八章,蛋糕之后是什么?中的脚本。
首先,我们需要从GameManagerScript中删除waypoints变量(但不要删除脚本,即使它是空的,因为我们还会使用它;出于同样的原因,不要从PandaScript中删除gameManager变量)。
现在,我们需要创建一个新的脚本,它将是实际上的航点。因此,我们可以将其重命名为Waypoint。
我们需要一个相同类的变量来存储下一个航点。这样,每个航点都将能够指向/引用另一个航点。目标是构建一个熊猫将跟随的链。由于变量是私有的,但我们仍然需要在检查器中访问它,我们需要添加可序列化的属性。因此,我们可以向我们的脚本中添加以下内容:
*//Private variable to store the next waypoint in the chain
//It is serializable, so it can be set in the Inspector*
[SerializeField]
private Waypoint nextWaypoint;
现在,从航点出发,熊猫想要在到达当前航点后检索其位置和下一个要跟随的航点。为了实现这一点,我们可以从我们的Waypoint脚本中公开两个函数。
GetPosition()函数将返回一个包含航点位置的Vector3,在这个特定的实现中,位置存储在航点的 Transform 中。代码如下:
*//Function to retrieve the position of the waypoint*
public Vector3 GetPosition() {
return transform.position;
}
相反,GetNextWaypoint()函数将仅返回下一个航点(至少目前是这样),存储在nextWaypoint变量中。实际上,nextWaypoint变量是私有的,因此熊猫需要一个函数来检索它。因此,我们可以简单地编写以下代码:
*//Function to retrieve the next waypoint in the chain*
public Waypoint GetNextWaypoint() {
return nextWaypoint;
}
目前我们已经完成了这个脚本,所以我们可以保存它。
下一步是创建我们的路标点的 Prefab。创建一个空 GameObject 并附加 Waypoint 脚本。然后,在 项目 面板中创建一个名为 WaypointPrefab 的 Prefab,并将你创建的空 GameObject 拖放到那里。最后,从场景中删除空 GameObject,因为我们已经有了我们的 Prefab。
拖放与已识别路标点数量相同的 Prefabs;在我们的例子中,有 11 个。为了方便起见,我建议你按顺序重命名它们,如下面的截图所示:

现在,我们需要将它们相互链接。特别是,waypoint1 将链接到 waypoint2,waypoint2 将链接到 waypoint3,依此类推。例如,waypoint4 在 检查器 中应该看起来如下链接:

唯一的例外是在最后一个路标点,nextWaypoint 变量中没有内容,如下面的截图所示:

最后,我们需要将它们放置在 获取路标坐标 部分中我们已确定的坐标上。为了快速识别它们,我建议你添加一个 Gizmo 图标。正如其名所示,Gizmo 是一个在场景视图中显示的图标,可以快速轻松地识别特定对象,但在游戏构建后不可见。最*,Unity 还增加了在 游戏 视图中查看它们的可能性。
插入 Gizmo 最简单的方式是点击 GameObject 名称旁边的立方体形状图标,如下面的截图所示:

注意
对于 Prefabs 也是如此,但它们的图标是一个蓝色立方体。
一旦点击此图标,就会出现一个菜单,如下面的截图所示:

通过选择一个椭圆形状的图标,你将为对象放置一个带有其名称的标签。我们将选择这些之一用于我们的路标点。如果你点击圆形或水晶形状的图标,Gizmo 将看起来像一个圆圈或水晶,没有任何文本。如果你点击 其他… 按钮,你可以使用你自己的图形。
注意
插入 Gizmos 的更复杂方式是通过脚本。实际上,有一个名为 OnDrawGizmos() 的特殊函数,当启用 Gizmos 渲染时,Unity 会调用该函数。在这个函数内部,你可以使用 docs.unity3d.com/ScriptReference/Gizmos.html 中列出的任何函数,这些函数允许你在屏幕上绘制形状。这是一个非常强大的工具,因为它可以极大地增强你脚本的可用性。例如,在我们的特定案例中,我们可以绘制 Pandas 将会跟随的路径。这被留作 作业 部分的练习。
在我们的情况下,我们可以为所有航点选择一个椭圆形图标。因此,我们能够在场景视图中看到它们(即使它们没有任何显式的渲染组件,因此在最终游戏中将无法以任何方式可见)并快速放置它们。
最后,你的场景视图应该看起来像以下这样:

现在,我们需要告诉游戏,这些航点中的哪一个是最初的链。为此,我们可以在游戏管理器中存储这些信息。所以,让我们向GameManagerScript添加以下变量:
*//The first waypoint of the chain*
public Waypoint firstWaypoint;
最后,在保存脚本后,在检查器中设置变量,如图下截图所示:

总之,我们已经创建了一个航点链,这正是我们游戏所需要的。然而,我们仍然需要定义熊猫如何到达它们。
沿着设计的路径移动 - 动态
下一步是稍微修改PandaScript以处理这个新的航点系统。所以,让我们再次打开脚本。
首先,我们需要用适当的航点变量替换整数变量currentWaypointNumber,如图所示:
*//Private reference to the current waypoint*
private Waypoint currentWaypoint;
然后,我们需要初始化这个新变量;我们可以在Start()函数中完成,通过从游戏管理器获取第一个航点,如图所示:
*//Get the first waypoint from the Game Manager*
currentWaypoint = gameManager.firstWaypoint;
然后,在FixedUpdate()函数的第一个检查中,我们需要检查变量本身是否为 null(这意味着熊猫已经到达了蛋糕,因为最后一个航点将返回一个 null 指针)。以下是代码,其中修改的部分已突出显示:
if (currentWaypoint == null) {
animator.SetTrigger(AnimEatTriggerHash);
Destroy(this);
return;
}
在FixedUpdate()函数中继续进行,我们需要更改距离的计算方式,通过以下方式使用我们的航点的GetPosition()函数:
float dist = Vector2.Distance(transform.position,
currentWaypoint.GetPosition());
最后,我们需要更改FixedUpdate()函数中的最后一个if语句,以便在到达前一个航点时获取下一个航点。我们还需要决定应该将哪个参数提供给我们的MoveTowards()函数。再次,修改的部分已突出显示:
if(dist <= changeDist) {
currentWaypoint = currentWaypoint.GetNextWaypoint();
}else {
MoveTowards(currentWaypoint.GetPosition());
}
保存脚本。我们已经完成了第二种实现航点的方式。第八章,“蛋糕之后是什么?”,将建议一些利用这种结构来实现更复杂行为的方法。
关于游戏中的更多人工智能内容
在前面的章节中,我们看到了几个在游戏环境中移动角色的航标系统实现。然而,正如我们在引言中已经说过的,这甚至没有触及游戏 AI 的表面。本节介绍了几个技术,但不会深入细节,因为它们对我们开发塔防游戏不是必需的。实际上,要掌握游戏中的 AI,你需要一本专门关于它的书。因此,您可以自由地跳过这一节,或者阅读它。您总是可以稍后回来,也许在您完成这本书后,更深入地了解其内容。本节的主要目标只是让您对游戏中的 AI 有一个基本的了解。
在寻路级别上的其他导航技术
本章开头关于寻路及其技术的部分绝不是详尽的,当然,在寻路级别上还有成千上万的其他技术被用于视频游戏中。
然而,特别值得一提的是:导航网格。这是重要的,因为它内置在 Unity 引擎的 3D 游戏中。这种技术背后的主要概念是对级别几何形状的预分析,以提取一个图(这是一个离线算法),其他寻路算法可以在需要时从中提取路径(这些算法是在线工作的)。
您可以通过在顶部菜单栏中点击窗口 | 导航来设置一些参数以生成这样的图表。具体来说,您可以在烘焙选项卡中设置通用选项,如下面的截图所示:

一旦构建了这个图表,代理可以通过脚本中的特定类来访问它。
无论如何,这都属于 Unity 的 3D 部分,我们不会进一步深入。但如果您想了解更多,可以从官方文档开始:docs.unity3d.com/Manual/Navigation.html(如您从目录中注意到的,这是 Unity 中一个相当庞大且功能强大的工具,但非常强大)。
转向行为级别的导航
记得我们如何在第五章,“物理学的小秘诀”中实现 Unity 中的物理方程吗?我们可以实现所有运动方程,我们将获得任何类型的运动。如果然后,我们将这个与目标、目的地或甚至只是一个方向混合,再加上一些避障技术,我们就已经实现了一种转向行为。
例如,如果你将障碍物避免实现为排斥 NPC 的磁铁,将目标实现为吸引磁铁,你将获得相当不错的转向行为。角色可以在没有路径寻找算法的情况下到达目的地。注意我说的是“可以”。实际上,他们可能会陷入困境,并且与转向行为作为最终解决方案相关的问题还有很多。但是,将转向行为整合到路径寻找算法中(第一个处理高级导航,例如从一个房间到另一个房间,第二个可以在房间内导航以到达下一个房间的门)是一个很好的补充,这可以带来非常逼真的行为,并且性能开销非常低。

磁铁转向行为的示例
通常,目标的吸引力范围扩展到整个地图,而障碍物的排斥力仅限于局部。此外,这些力可以遵循不同的势能定律,并具有不同的形状。为了帮助你可视化磁场,你可以看看 Dayna Mason 提供的图片:www.flickr.com/photos/daynoir/2180507211。每个指南针代表当角色处于该位置时所受到的力的类型。在我们的例子中也是如此,角色被障碍物推开,被目标吸引。值得注意的是,我们的目标和障碍物是单极磁铁,但在我们的物理世界中它们并不存在(只有磁偶极子存在)。
导航在路径寻找/决策制定层面 – 基于信念的路径寻找
正如我之前提到的,学术界最*开始将路径寻找中的决策部分进行整合。我的一个关于基于信念的路径寻找(BDP)的出版物就是这样一个例子。
关键概念是 NPC 不一定知道整个地图。想象一下有一条桥可以跨越河流,但玩家已经摧毁了这座桥;因此,当角色在环境中导航时,角色应该将地图视为桥还在那里,因为他不知道桥已经塌陷。只有当他接*河流时,他才意识到桥已经塌陷,因此,采取行动(例如找到另一条路,建造另一座桥,用木头制作一艘筏子,或者游泳)。这就是为什么它是基于信念的,因为角色按照他/她相信的环境进行导航,并据此做出假设。
注意
如果你对了解更多关于 BDP(行为决策规划)感兴趣,你可以查看我的网站:francescosapio.com。
超越导航
游戏中的 AI 不仅限于导航,正如我们在引言中提到的,AI 在视频游戏中可以应用于许多层面。想象一下回合制游戏,其中 NPC 需要做出战略决策。
但人工智能不仅适用于非玩家角色。有些游戏实现了适应游戏难度的算法(自适应和学习算法),有些处理摄像机应该如何移动以在玩家中唤起特定的情感状态(例如,在 Georgios N. Yannakakis 的工作中特别如此,在论文《空间迷宫:基于经验的游戏摄像机控制》中)。其他游戏有程序内容生成(PCG)的算法,如著名的Temple Run(Imangi 工作室,2011 年)通过程序生成关卡,或者甚至是Minecraft(Mojang,2011 年)中整个世界都是通过程序生成的。
人工智能也应用于游戏和玩家的分析,例如研究游戏的内部结构或收集玩家的心理档案。关于后者,您需要想象在严肃游戏中的应用,在这些游戏中,游戏可以用来评估特定环境中人们的表现。您可以在 Lauren S. Ferro 撰写的有趣的开篇论文《迈向个性化的游戏化系统:对游戏设计、个性和玩家类型的调查》中找到相关信息(可在dl.acm.org/citation.cfm?id=2513024找到)。
最后,值得一提的是,游戏与人工智能之间的关系不是单向的。事实上,不仅游戏使用人工智能,反之亦然。一些关于人工智能的研究和研究表明,视频游戏有助于提供完美的模拟环境(例如,为机器人),并且还可以创建替代玩家并玩游戏(不涉及作弊,因为游戏中的 AI 总是使用额外的数据来收集游戏的具体状态)。
在任何情况下,请记住,当人工智能应用于游戏时,最重要的目标不是追求现实感,而是为玩家创造一个沉浸式和娱乐性的体验(如果游戏设计包含这一目标,这也可能导致学习体验)。
作业
在本章中,我们概述了游戏中的人工智能。然而,我们只关注了导航,特别是为我们的游戏实现航点系统。但您仍然可以对其进行改进,本节提出了一些练习来实现这一点。因此,在下一章之前,我邀请您完成以下练习,以进一步提高您的技能:
-
成为 AI 设计师和程序员:想想你玩的五款游戏,并选择其中包含非玩家角色(NPC)的部分,比如敌人或甚至 Boss。现在,列出它们各自的行为清单。现在,移除一些行为,甚至添加一些,并思考这会如何改变体验。它是改善了体验,还是完全改变了氛围?你能通过改变一些行为,将相对真实的行为变成智能的,反之亦然吗?通过这样做,你将开始理解某些行为在不仅为你的角色提供生命,而且在为玩家提供情感方面的重要性。
-
航点作为碰撞体:在前一章中,我们学习了如何使用碰撞体和检测碰撞。特别是,我们看到了糖果是如何与熊猫碰撞以触发动作的(在这种情况下,是射下熊猫)。然而,同样的原理也可以应用在这里。相反,为了使用
changeDist常数并检查熊猫到航点的距离,我们可以再次使用OnTriggerEnter2D()函数来检查熊猫何时到达航点。以这种方式实现航点的改变,无论你使用的是第一个还是第二个实现。小提示:你可能需要为航点设置一个新的标签(记得如何在第二章中设置标签,制作纸杯蛋糕塔?),并在它们上添加碰撞体。 -
自动到达的航点:在更复杂的航点系统中,你不应该手动创建链(即使当地图变大时,解决方案变得难以扩展)。因此,尝试设计和实现一个系统,其中航点一旦放置就会自动连接。
-
创建 Gizmo 路径显示器:在第二章中,我们介绍了如何使用一些 Gizmo 函数在场景上绘制有用的东西。使用这些函数,特别是
Gizmos.DrawLine(),可以在 场景 视图中显示航点的链。最后,有一个挑战给你:
-
简单航点:在第二个实现中,我们为每个航点使用不同的游戏对象。设计和实现一个系统,它对设计师来说尽可能简单(这样他们可以在 场景 视图中拖动航点,并可能看到之前练习中的链),同时效率高,因为它不会使用游戏对象作为航点(而是存储在某个地方的数组)。
摘要
在本章中,我们学习了视频游戏中人工智能的非常基础的知识。我们概述了导航和一些常用的技术。
然后,我们基于航点构建了我们游戏的导航系统。特别是,我们为我们的游戏实现了两种类型的航点系统,以学习实现相同结果的不同方法。
最后,我们对游戏中的 AI 又进行了另一个总的概述。
在下一章中,我们将完成我们的游戏!已经兴奋了吗?好吧,你在等什么呢?下一章就在一页之隔。
第七章. 交易纸杯蛋糕和终极蛋糕争夺战 – 游戏玩法编程
在了解了 Unity 引擎的不同部分之后,是时候回到我们的游戏并完成它了。在这个过程中,我们将探讨另一个重要主题:游戏玩法编程。特别是,我们将看到数据如何在游戏的不同部分之间交换。由于在我们的塔防游戏中还有许多部分需要连接,并且它们可以以数千种不同的方式实现,我尝试选择不同的技术,以便给你一个在 Unity 中实现事物的新视角。
具体来说,在本章中我们将涵盖:
-
如何实现一个交易系统,允许玩家购买、出售和升级纸杯蛋糕塔
-
编写脚本,以便玩家在购买后可以在地图上放置纸杯蛋糕塔
-
如何触发游戏结束条件以显示胜利或失败屏幕
-
跟踪玩家在关卡中取得的进展
-
根据将熊猫分为波次来创建我们的熊猫的生成系统
-
设计和实现主菜单
-
如何在 Unity 中更改场景
和往常一样,在章节的结尾你可以找到作业部分,其中充满了练习,帮助你将技能提升到更高的水*。其中一些练习将挑战你对章节中学到的概念的理解,并将指导你改进我们的塔防游戏。
但在我们直接进入游戏玩法编程之前,让我们花些时间了解一般性的游戏玩法编程。
准备工作
为了更好地理解本章,你应该已经阅读了所有其他章节,因为我们将在这里实现我们的脚本,这些脚本是在本书中之前创建的。
为了简单起见,对于那些只在上一章中进行了第一次实现的读者,我保留了第一次实现的代码。然而,对于那些进行了第二次实现的读者,你应该没有问题对代码进行相同的修改。无论如何,建议你在游戏中保留代码的第二次实现,因为下一章将给我们一些方法来利用其在更大游戏环境中的潜力。
游戏玩法编程是什么意思?
游戏玩法编程并没有一个独特的定义。当然,它涉及到游戏的建设和发展,但例如,它是否包括人工智能编程?或者用户界面?或者数据库连接?或者动画机器?因此,定义取决于上下文。然而,值得注意的是,当你寻找工作职位时,定义又会发生变化。一般来说,根据经验,公司越大,游戏玩法程序员的工作职位越好。实际上,在小公司中,团队可能资源有限,因此拥有一个游戏玩法程序员意味着他/她将做所有事情,而在有 AI、UI 和动画程序员的较大团队中,游戏玩法程序员更可能成为所有这些角色的协调者,因此这是一个更高的职位。
我想用一位游戏开发者的博客中的一句话来结束本节(游戏开发者博客),他描述了作为一名游戏玩法程序员的感觉:
"正如你所见,成为一名游戏玩法程序员不仅仅是解决问题,甚至比设计师还要多——你必须真正弄清楚问题的所有方面并解决它们。但你也有更深入的了解整个系统是如何工作的。你可以创建这些系统。你可以成为第一个看到令人惊叹的事情发生的人。"
现在,是时候深入细节,完成我们的塔防游戏了!
规划我们游戏剩余要实现的内容
在我们完成游戏之前,首先要做的是整理我们已经写下的想法。
在第一章,Unity 中的*面世界中,我们导入了所有精灵并正确设置了它们。然后,在第二章,制作纸杯蛋糕塔中,我们实现了投射物和纸杯蛋糕塔类。在第三章,与玩家沟通——用户界面中,我们实现了玩家的生命值和糖分,而在第四章,不再孤单——甜食熊猫出击中,我们专注于为我们的甜食熊猫制作动画。最后,在第五章,秘密成分是物理学的一点点中,我们探讨了物理学以及我们如何在游戏中使用它,并在第六章,穿过糖霜的海洋——人工智能中的导航中,我们给了熊猫沿着通往甜蛋糕的路径移动的可能性。
那么,接下来要做的有以下几点:
-
集成一个交易系统,以便玩家可以购买、出售和升级纸杯蛋糕塔
-
创建一个机制,以便在购买后放置纸杯蛋糕塔
-
指定玩家如何选择特定的纸杯蛋糕塔
-
设置游戏结束条件。
-
在游戏过程中跟踪玩家的进度
-
为我们的熊猫实现一个生成系统
-
为我们的游戏创建一个主菜单
在完成所有这些之后,我们基本上将有一个功能齐全的游戏。所以,让我们从列表中的第一个开始。
交易纸杯蛋糕塔
在本节中,我们将了解如何允许玩家进行塔的交易。特别是,玩家可以购买、出售或升级一个纸杯蛋糕塔。由于这三个动作有一些共同点,我们将通过使用继承来实现它们。如果你还记得,我们在第二章,制作纸杯蛋糕塔中谈到了一点,但现在我们有机会看到它付诸实践。因此,在这个过程中,我们将更好地理解抽象方法和静态变量,因为我们将再次使用它们。
在任何情况下,玩家可以执行的所有这些交易动作都是单独实现的。这是我们将会实现的架构:

每个子脚本都可以附加到一个 UI 元素上,这将转换成一个按钮来执行该特定动作。让我们从父类开始。
交易父类
让我们开始创建一个名为TradeCupcakeTower的通用类,并使用你最喜欢的代码编辑器打开它。
购买、出售和升级需要用户在用户界面中点击他们的图标(我们将在脚本整个交易系统完成后创建场景中的 UI 以执行交易动作),因此我们需要提供一种检测玩家点击(或移动应用中的轻触)的方法。正如我们在第三章,与玩家沟通——用户界面中讨论的那样,我们可以使用一个处理器。因此,我们需要在脚本开头添加以下库:
using UnityEngine.EventSystems;
现在,在类的定义中,我们可以添加点击处理器。此外,由于这将是一个抽象类,我们需要以下方式指定它:
public abstract class TradeCupcakeTowers : MonoBehaviour,
IPointerClickHandler {
在交易时,我们想要检查玩家的糖分水*(这是我们游戏的货币)。因此,我们需要有一个对糖量计的参考,它也是所有交易类共享的。因此,我们可以使这个变量成为受保护的静态变量:
*// Variable to store the Sugar Meter*
protected static SugarMeterScript sugarMeter;
类似于我们在上一章中必须获取所有 Pandas 的游戏管理器引用时所做的(因为在这种情况下变量也是静态的),我们只需要获取一次糖量计的引用。因此,在Start()函数中,我们可以编写:
void Start () {
*//If the reference to the Sugar Meter is missing, the script gets it*
if (sugarMeter == null) {
sugarMeter = FindObjectOfType<SugarMeterScript>();
}
}
当玩家出售或升级一座塔时,交易系统应该知道玩家指的是哪座塔(玩家如何选择塔留待选择塔部分稍后讨论)。因此,我们再次可以使用所有交易操作类共享的受保护和静态变量:
*//Variable to store the current selected tower by the player*
protected static CupcakeTowerScript currentActiveTower;
然后,我们需要一个函数来设置选择(当前活动塔),并且它需要是静态的,这样其他脚本就可以轻松设置(正如我们稍后将会看到的)。该函数只是将作为参数传递的塔赋值给静态变量:
*// Static function that allows other scripts to assign the new/current
selected tower*
public static void setActiveTower(CupcakeTowerScript cupcakeTower) {
currentActiveTower = cupcakeTower;
}
最后,我们需要实现处理点击的接口。然而,应该执行的动作序列取决于玩家是在购买、出售还是升级。因此,我们可以将实现留给子类,并将这个作为抽象函数(参考第二章,制作纸杯蛋糕塔,了解抽象和虚函数的工作方式)如下所示:
*// Abstract function triggered when one of the trading buttons is
pressed, however the
// implementation is specific for each trade operation.*
public abstract void OnPointerClick(PointerEventData eventData);
我们可以保存脚本,因此我们的父类就准备好了。现在,在我们实现其子类以执行玩家可以执行的具体交易动作之前,我们需要修改CupcakeTowerScript。
修改 CupcakeTowerScript
回到第二章,制作纸杯蛋糕塔,我们为我们的纸杯蛋糕塔实现了许多功能。然而,在它们的脚本上还有更多的工作要做。特别是,我们需要添加一些变量来存储它们的价格和成本。
让我们从添加以下自解释的变量开始,我们可能希望在稍后将其设置到检查器中:
*// How much this tower costs when it is bought *
public int initialCost;
*// How much this tower costs when it is upgraded*
public int upgradingCost;
*// How much this tower is valuable if sold*
public int sellingValue;
每当我们升级杯子蛋糕塔时,我们希望同时提高sellingValue(因为升级后的塔更有价值),以及UpgradingCost(因为升级到更高等级需要更多的糖)。因此,我们可以在Upgrade()函数中添加以下代码行(这些值可能取决于你游戏中非常具体的*衡,但下一章将更详细地介绍这一点以及如何以动态方式处理塔的成本):
*//Increase the value of the tower;*
sellingValue += 5;
//Increase the upgrading cost
upgradingCost += 10;
保存脚本,进入你的杯子蛋糕塔预制体,并在检查器中更改这三个新变量的值(当然,你可以使用你喜欢的值)。以下是一个示例:

现在,我们已经准备好实施交易动作,从购买开始。
购买杯子蛋糕塔
在本节中,我们将实现处理购买动作的脚本。让我们先创建一个新的脚本,命名为TradeCupcakeTowers_Buying,然后打开它。
首先,我们仍然需要从 Unity 引擎中导入事件系统库:
using UnityEngine.EventSystems;
在类声明中,我们需要指定我们将要扩展TradeCupcakeTowers类,而不是MonoBehaviour,如下所示:
public class TradeCupcakeTowers_Buying : TradeCupcakeTowers {
如果你回顾一下第三章中的用户界面设计,与玩家沟通——用户界面,我们有三种不同的塔供玩家购买。每个按钮一旦被点击,就会实例化一个不同的塔。因此,我们需要指定这个脚本的实例引用的是哪个杯子蛋糕塔预制体。当然,其值应该在检查器中设置(我们稍后会看到这一点)。所以,让我们添加以下变量:
*/* Public variable to identify which tower this script is selling.
* Ideally, you could have many instances of this script selling
different
* Cupcake towers, and the tower is specified in the Inspector */*
public GameObject cupcakeTowerPrefab;
然后,我们需要实现从其父类继承的抽象函数,以处理玩家点击其图标时发生的情况。为此,我们需要使用override属性,并以下述方式声明方法:
public override void OnPointerClick(PointerEventData eventData) {
* //Rest of the code*
}
现在,当玩家点击时首先要做的是检索玩家想要购买的杯子蛋糕塔的价格:
*//Retrieve from the prefab which is its initial cost*
int price = cupcakeTowerPrefab.GetComponent<CupcakeTowerScript
().initialCost;
接下来,我们需要通过使用共享静态变量sugarMeter来检查玩家是否有足够的糖。如果玩家有足够的糖,则实例化一个新的杯子蛋糕塔(我们将在本章后面看到玩家如何放置塔),并将其分配为交易类中的活动塔:
*// Check if the player can afford to buy the tower*
if (price <= sugarMeter.getSugarAmount()) {
* //Payment succeeds, and the cost is removed from the player's sugar*
sugarMeter.ChangeSugar(-price);
* //A new cupcake tower is created*
GameObject newTower = Instantiate(cupcakeTowerPrefab);
* //The new cupcake tower is also assigned as the current selection*
currentActiveTower = newTower.GetComponent<CupcakeTowerScript>();
}
保存脚本,购买功能已实现。让我们看看玩家如何通过出售杯子蛋糕塔来获取一些糖分。
出售杯子蛋糕塔
在本节中,我们将实现处理销售动作的脚本。创建一个新的脚本,命名为TradeCupcakeTowers_Selling,然后打开它。
再次强调,我们仍然需要从 Unity 引擎中导入事件系统库:
using UnityEngine.EventSystems;
与TradeCupcakeTowers_Buying一样,我们需要以下方式从TradeCupcakeTowers类中继承:
public class TradeCupcakeTowers_Selling : TradeCupcakeTowers {
然后,我们需要实现抽象函数,以处理玩家点击销售图标时发生的情况。再次,我们需要使用 override 属性,如下所示:
public override void OnPointerClick(PointerEventData eventData) {
*//Rest of code*
}
由于销售是玩家始终能够执行的操作,我们不需要进行任何检查(除非有一个活动的塔),而是检索纸杯蛋糕塔的价值,并将其金额添加到玩家的储蓄中。然后,从场景中移除纸杯蛋糕塔:
*//Check if there is a tower selected before to proceed*
if (currentActiveTower == null)
return;
*//Add to the player's sugar the value of the tower*
sugarMeter.ChangeSugar(currentActiveTower.sellingValue);
*//Remove the cupcake tower from the scene*
Destroy(currentActiveTower);
最后,我们可以保存这个脚本。因此,销售功能也得到了实现。只剩下升级功能未完成。
升级纸杯蛋糕塔
在这里,我们需要创建升级按钮。创建一个脚本,命名为 TradeCupcakeTowers_Upgrading,然后打开它。
再次强调,我们仍然需要从 Unity 引擎中导入事件系统库:
using UnityEngine.EventSystems;
就像我们对其他交易类所做的那样,我们需要以下方式从 TradeCupcakeTowers 类中继承:
public class TradeCupcakeTowers_Upgrading : TradeCupcakeTowers {
然后,我们需要实现抽象函数,以处理玩家点击升级按钮时发生的情况。再次,我们需要使用 override 属性,如下所示:
public override void OnPointerClick(PointerEventData eventData) {
* //Rest of the code*
}
与我们处理购买按钮的方式类似,我们需要检查玩家是否有能力升级塔,以及塔是否真的可以升级(我们有一个布尔标志用于此,设置在第二章,制作纸杯蛋糕塔)。如果是这样,升级的费用将从玩家的糖分中扣除,塔最终升级:
*//Check if the player can afford to upgrade the tower*
if(currentActiveTower.isUpgradable && currentActiveTower.upgradingCost
<=sugarMeter.getSugarAmount()) {
* //The payment is executed and the sugar removed from the player*
sugarMeter.ChangeSugar(-currentActiveTower.upgradingCost);
*//The tower is upgraded*
currentActiveTower.Upgrade();
}
保存这个脚本,结果我们就完成了所有的交易功能。然而,它们在场景中并不存在,所以让我们将它们添加到我们的界面中。
将交易选项添加到用户界面
现在我们有了实现所有不同交易按钮的脚本,我们需要实际上将它们放置在我们的场景/级别中。
因此,让我们开始创建三个 UI 图像,并将 TradeCupcakeTowers_Buying 脚本附加到每个图像上。作为它们的源图像,你可以选择我们图形包中为三种不同类型的塔提供的图标。如果你没有实现所有这些,没关系,只需删除你不需要的按钮。相反,如果你使用自己的图形实现了更多,请随意添加更多这些按钮。然后,在你正确缩放按钮后,将它们放置在以下截图所示的界面中:

然后,在检查器中,我们需要分配它们各自的纸杯蛋糕塔预制体。这里只展示了三个按钮中的一个,作为示例供你参考:

很好,现在玩家可以购买塔了!那么关于销售和升级它们呢?
让我们再创建另外两张 UI 图像,并将分别附上TradeCupcakeTowers_Selling和TradeCupcakeTowers_Upgrading脚本。然后,使用我们在图形包中可以找到的用于销售和升级的图标作为源图像。适当地调整按钮大小,并将它们放置在以下截图所示的用户界面中:

在检查器中我们没有任何变量需要分配,因此我们可以认为我们的交易系统已经准备好了!尽管要使其正常工作,我们仍然需要一种放置塔楼的方法以及选择塔楼的方法。这些内容将在下一节中进行探讨。
放置塔楼
一旦玩家购买了纸杯蛋糕塔,他或她应该能够决定将其放置在哪里。本节将探讨如何实现这一机制,这可能很简单,但需要你注意许多细节。
绘制工作原理的草图
我们可以通过多种方式实现这个系统,但我们将使用碰撞器和纸杯蛋糕塔上的第二个脚本。因此,你还将学会处理不同游戏元素之间信息交换的新方法。
特别地,我们将定义一些允许放置塔楼的区域,我们将通过使用碰撞器来实现这一点。然后,游戏管理器会注册玩家的指针是否在允许区域内。第二个脚本附加到纸杯蛋糕塔上,它使用游戏管理器提供的信息来实际上允许玩家放置纸杯蛋糕塔。此外,一旦塔楼放置好,脚本会为纸杯蛋糕塔附加一个碰撞器。这将防止塔楼被放置在其他塔楼之上,并且对于实现选择系统也将非常有用。
注意
在作业部分,你将找到一些练习来提高我们将在本节中实现的内容。
允许区域
首先,我们应该注意到玩家不能在地图上的任何地方随意放置他的塔楼。实际上,他不能在熊猫移动的路径上或在水或其他障碍物所在的区域放置塔楼。因此,我们需要在我们的游戏中指定这个限制。因此,我们需要查看我们的地图,并找到玩家可以放置塔楼的所有位置。在我们的案例中,我们正在寻找的位置如下:

如我们所见,它们具有自定义的形状。即使可以实现自定义形状(这留给想要挑战自己的读者作为练习),但以矩形的形式思考并因此将我们的形状分割成矩形可能会更加方便。当然,这可以以多种方式完成;然而,覆盖整个区域的矩形越少,从计算的角度来看就越好。另一方面,通过使用更多的矩形,你能够更好地*似你的区域。所以找到你的权衡点。
一个可能的选择如下:

最后,我们找到了 11 个区域。
这里的想法是,所有这些区域都连接到Game Manager对象上的2D 盒子碰撞器,它将通过切换一个标志来检查鼠标是否位于这些区域之一。这个标志将被我们在下一节中要实现的脚本读取。
让我们从在游戏管理器上添加一个2D 盒子碰撞器开始,方法是点击组件 | 物理效果 2D | 2D 盒子碰撞器。然后,我们需要将其调整到与我们所找到的矩形相同的尺寸,并使用偏移参数将其放置到地图上。在这个阶段,你应该能够重复此操作以覆盖地图的所有区域,而无需在此书中写下它们的精确值。
现在,下一步是修改GameManagerScript以切换标志。一旦我们打开了脚本,我们就可以添加标志作为一个布尔变量:
*//Private variable to check if the mouse is hovering an area where*
*//Cupcake tower can be placed*
private bool _isPointerOnAllowedArea = true;
由于我们不希望其他脚本更改此变量,它是私有的,因此我们需要提供一个函数来检索其值:
*//Function that returns true if the mouse is hovering an area where a
//Cupcake tower can be placed*
public bool isPointerOnAllowedArea() {
return _isPointerOnAllowedArea;
}
Unity 为我们提供了一些非常实用的函数来检测玩家的指针是否进入了一个区域。它们的名称是自解释的:OnMouseEnter()和OnMouseExit()。在第一个函数中,我们将标志设置为true,而在第二个函数中,我们将标志设置为false:
*//Function which is called when the mouse enters in one of the
//colliders of the Game Manager*
void OnMouseEnter() {
*//Set that the mouse is now hovering an area where placing Cupcake
//towers is allowed*
_isPointerOnAllowedArea = true;
}
*//Function which is called when the mouse exits from one of the
//colliders of the Game Manager*
void OnMouseExit() {
* //Set that the mouse is not hovering anymore an area where placing
//Cupcake towers is allowed*
_isPointerOnAllowedArea = false;
}
保存脚本,允许区域的设置就绪。
编写放置脚本
在购买蛋糕塔之后放置蛋糕塔,我们需要为我们的蛋糕塔创建另一个脚本。你可以将其重命名为PlacingCupcakeTowerScript,并将其添加到蛋糕塔预制体中。
在修改它之前,我们需要从我们的蛋糕塔预制体中取消选中CupcakeTowerScript。实际上,一座塔第一次进入场景是因为玩家购买了它。在放置模式下,蛋糕塔不应该发射。一旦放置,CupcakeTowerScript被启用,塔再次开始运作。
现在,我们可以打开新创建的脚本。我们需要检索Game Manager,因为我们需要它来检查鼠标是否位于可以放置蛋糕塔的区域。为此,我们可以编写以下代码,这是我们在第六章中使用的相同代码,穿越糖浆之海 – 人工智能导航,以首次检索Game Manager:
*// Private variable to store the reference to the Game Manager*
private GameManagerScript gameManager;
void Start () {
* //Get the reference to the Game Manager*
gameManager = FindObjectOfType<GameManagerScript>();
}
在Update()函数中,我们将塔移动到鼠标位置(因此,在每一帧,塔都会随着玩家的鼠标移动),如果玩家按下键,我们检查指针是否实际上在允许的区域。如果是这样,塔就被放置了,这意味着移动塔的脚本被销毁。此外,CupcakeTowerScript再次启用,并在蛋糕塔上放置了一个碰撞器。实际上,这个额外的碰撞器防止了其他塔放在这个塔的上面(以及在下一节中选择塔):
void Update () {
*//Get the mouse position*
float x = Input.mousePosition.x;
float y = Input.mousePosition.y;
*/* Place the cupcake Tower where the mouse is, transformed in game
coordinates
* from the Main Camera. Since the Camera is placed at -10 and we
want the
* tower to be at -3, we need to use 7 as z-axis coordinate */*
transform.position = Camera.main.ScreenToWorldPoint(new Vector3(x,
y, 7));
*//If the player clicks, the second condition checks if the current
position is
//within an area where cupcake towers can be placed*
if (Input.GetMouseButtonDown(0) &&
gameManager.isPointerOnAllowedArea()) {
* //Enabling again the main cupcake tower script, so to make it
operative*
GetComponent<CupcakeTowerScript>().enabled = true;
* //Place a collider on the Cupcake tower*
gameObject.AddComponent<BoxCollider2D>();
*//Remove this script, so to not keeping the Cupcake Tower on the
mouse*
Destroy(this);
}
保存脚本后,玩家一旦购买,就可以放置蛋糕塔。
选择塔
如果你记得,所有的交易操作都有一个选定的塔来处理。实际上,当玩家按下卖出按钮时,游戏应该知道玩家打算卖出哪个蛋糕塔。因此,玩家应该能够选择(并取消选择)一个塔,并且这个塔应该通知交易系统。
为了实现这一点,我们需要稍微修改一下CupcakeTowerScript。从上一节中,我们知道当塔处于活动状态时,它有一个碰撞器来防止其他塔放在它的上面。但是,我们也可以使用这个碰撞器来检测玩家是否点击了这个非常具体的塔。特别是,我们可以用以下方式使用自解释的函数OnMouseDown():
*//Function called when the player clicks on the cupcake Tower*
void OnMouseDown() {
* //Assign this tower as the active tower for trading operations*
TradeCupcakeTowers.setActiveTower(this);
}
保存脚本后,玩家能够选择他在游戏中拥有的特定塔,并通过交易系统进行出售或升级。
游戏管理器
在上一章中,我们介绍了GameMangerScript,即使在第二次实现航标点之后,我们仍然让这个脚本保持空白,没有任何用途。然而,我们确实需要在我们的游戏中有一个游戏管理器来处理一些事情。所以,如果你在上一个章节中将其删除,请重新创建它,并在场景中创建一个带有此脚本的游戏对象(就像你从PandaScript中删除引用一样,因为我们稍后会用到它)。
我们将使用游戏管理器作为玩家健康状态和熊猫之间交换信息的中心。实际上,游戏管理器会在场景中分波生成熊猫,并且它是唯一在关卡开始和结束以及/或者玩家失去所有健康时需要关注的脚本。这使得游戏管理器成为处理和触发游戏结束条件的理想人选。让我们从这些开始。
游戏结束条件
我们的游戏何时结束?嗯,有两种情况:当玩家失去健康,这意味着熊猫吃掉了所有的蛋糕(失败条件),或者当玩家击落所有熊猫(胜利条件)。在任何一种情况下,我们都需要向玩家展示结果并终止游戏。
游戏结束反馈
在我们的图形包中,有两个屏幕在游戏结束时准备就绪。分别是游戏结束,用于失败条件,和你赢了,用于胜利条件。
创建两个 UI 图像,正如我们在第三章“与玩家沟通——用户界面”中学到的,放置我们包中的两个精灵,每个 UI 图像一个。你可能想按设置原生大小按钮,然后调整大小和位置,使它们位于场景中间,如图所示:

现在,我们可以禁用它们,因为它们不应该在游戏结束之前显示。然而,我们需要在Game Manager中添加对它们的引用。
因此,打开GameManagerScript并添加以下变量:
*//Variable to store the the screen displayed when the player loses*
public GameObject losingScreen;
*//Variable to store the screen displayed when the player wins*
public GameObject winningScreen;
保存脚本,然后在检查器中分配我们之前创建的 UI 图像,如图所示:

因此,当满足某些条件时,Game Manager能够激活其中的一个。让我们在下一节中看看如何实现这个功能的函数。
GameOver 函数
为了在GameManagerScript中保持条理清晰,让我们创建一个函数来触发游戏结束时发生的事情。它将有一个布尔值作为参数,以确定玩家是否获胜。
注意
当然,游戏结束时确切会发生什么取决于你。你可以保存统计数据和分数(如果你有的话),触发漂亮酷炫的动画,显示加载下一级的按钮等等。在这本书中,我们只会显示上一节创建的 UI 图像,因为目标是向你展示在哪里以及如何插入游戏结束的代码。请随意添加你自己的实现。
因此,让我们写下这个函数,根据参数,将显示正确的屏幕给玩家。然后,它停止游戏的时间,在游戏中创建一种暂停情况。结果,当游戏结束屏幕出现时(如果有 UI 存在,玩家仍然可以点击它):
*//Private function called when some gameover conditions are met, and
displays
//the winning or losing screen depending from the value of the
parameter passed.*
private void GameOver(bool playerHasWon) {
*//Check if the player has won from the parameter*
if (playerHasWon) {
*//Display the winning screen*
winningScreen.SetActive(true);
}else {
*//Display the losing screen*
losingScreen.SetActive(true);
}
*//Freeze the game time, so to stop in some way the level to be
executed*
Time.timeScale = 0;
}
注意
你可以在 Unity 官方文档中了解更多关于timeScale的信息:docs.unity3d.com/ScriptReference/Time-timeScale.html
保存GameManagerScript,然后让我们在下一节中探讨何时触发此函数。
跟踪游戏的进度
跟踪游戏的进度是游戏管理器的基本功能之一。因此,我们首先想问的是:我们应该跟踪什么?
绝对不是玩家拥有的糖,因为糖在糖量计和交易脚本中单独处理。那么玩家的健康呢?嗯,我们确实想跟踪它。事实上,当玩家失去健康时,游戏也会结束,Game Manager需要处理这种情况。还有什么?Game Manager需要跟踪玩家击落了多少只大熊猫,因为这样,游戏管理者就能确定玩家何时获胜。
因此,我们首先需要做的是获取玩家的健康引用。我们可以添加以下变量:
*//Private variable to store the reference to the Player's health*
private HealthBarScript playerHealth;
我们可以在Start()函数中初始化它,在开始处添加以下行:
void Start () {
*//Get the reference to the Player's health*
playerHealth = FindObjectOfType<HealthBarScript>();
}
然后,我们需要一个变量来跟踪还有多少只大熊猫需要击败,因此我们可以添加以下变量:
*//Private variable which acts as a counter of how many Pandas are
remained to defeat*
private int numberOfPandasToDefeat;
它将由我们的生成系统初始化,我们将很快实现。
最后,我们需要实现几个函数,这些函数将在大熊猫被击落和玩家失去健康时分别被调用。
对于第一个功能,我们不需要任何参数或返回值,因为Game Manager只需确认一只大熊猫已被击落,通过减少还需要击败的大熊猫数量:
*//Function that decreases the number of Pandas still to defeat every
time a Panda dies *
public void OneMorePandaInHeaven() {
numberOfPandasToDefeat--;
}
关于第二个功能,我们希望在吃蛋糕的大熊猫和玩家的健康之间建立一个沟通中心。因此,我们需要实现一个函数,该函数接受大熊猫造成的伤害作为参数,并从玩家的健康中扣除。然后,它检查玩家是否仍然存活,因为如果他不/她不存活,就会触发GameOver函数。在任何情况下,我们都需要减少还需要击败的大熊猫数量,因为我们记得大熊猫吃了很多蛋糕以至于它们会爆炸:
*//Function that damages the player when a Panda reaches the player's
cake.
//Moreover, it monitors the player's health to trigger the GameOver
function when needed*
public void BiteTheCake(int damage) {
* //Apply damage to the player and retrieve a Boolean to see if the
cake has been eaten all*
bool IsCakeAllEaten = playerHealth.ApplyDamage(damage);
* //If the cake has been eaten all, the GameOver function is called in
"losing mode"*
if (IsCakeAllEaten) {
GameOver(false);
}
*//The Panda that bit the cake will also explode, and therefore we
have a Panda less to defeat*
OneMorePandaInHeaven();
}
保存脚本,并打开PandaScript,因为我们现在需要稍微修改它。特别是,我们需要在Game Manager中调用刚刚创建的函数。从第六章,《穿过糖雨——人工智能中的导航》,我们已经有了对Game Manager的引用,我们可以用它来触发这些函数。
第一个修改是添加以下变量以确定这只特定的大熊猫在咬蛋糕时可以吃掉多少蛋糕(其值需要在检查器中设置,别忘了!):
*//The amount of cake that the Panda eats*
public int cakeEatenPerBite;
第二个修改是在FixedUpdate()函数中。实际上,我们需要使用Game Manager中的BiteTheCake()函数来减少玩家的健康。下面高亮的部分是我们所做的修改:
void FixedUpdate() {
*//if the Panda has reached the cake, then it will eat it, by
triggering the right animation,
//and remove this script, since the State Machine Behaviour will take
care of removing the Panda*
if (currentWaypointNumber == gameManager.waypoints.Length) {
animator.SetTrigger(AnimEatTriggerHash);
gameManager.BiteTheCake(cakeEatenPerBite);
Destroy(this);
return;
}
* // [...] The remaining code of the function*
第三个也是最后一个修改是在Hit()函数中,我们还需要触发游戏管理器的OneMorePandaInHeaven()函数。我们可以这样做(再次,高亮的部分是我们所做的修改):
private void Hit(float damage) {
* //Subtract the damage to the health of the Panda*
health -= damage;
* //Then it triggers the Die or the Hit animations based if the Panda
is still alive*
if(health <= 0) {
animator.SetTrigger(AnimDieTriggerHash);
gameManager.OneMorePandaInHeaven();
}
else {
animator.SetTrigger(AnimHitTriggerHash);
}
}
保存脚本,因为我们将在下一节中探索熊猫是如何被创建/生成的。
熊猫入侵 – 生成熊猫
在本节中,我们将实现游戏的生成系统。这可以通过多种方式完成。然而,由于我们只有一种熊猫(至少目前是这样),我们将以简单的方式实现它。无论如何,我们将使用协程来实现系统,并且我们将看到一个模板结构,我们可能在更复杂的生成系统中也会使用(在下一章,将提供一些更复杂生成系统的想法)。
协程是什么?
这是一个 Unity 提供的一种结构,允许函数在游戏的其它帧中被中断并继续执行。在我们的生成系统中,我们不想一次性生成所有的大熊猫,而是希望它们在一段时间内逐渐生成。这个“一段时间”可以通过协程来控制。你肯定可以在官方文档中学习更多并查看一些示例:docs.unity3d.com/Manual/Coroutines.html
然而,关于协程最重要的几点如下:
-
它们是特殊的函数,返回值是一个
IEnumerator。 -
它们可以通过
StartCoroutine()函数启动,并通过StopCoroutine()函数停止。 -
它们不能在任何
Update()函数中运行/启动。原因是Update()函数的本质是每帧(或更多)被调用一次,而协程的本质是在它们指定的时刻运行。 -
它们可以使用一个特殊的指令;
yield:它允许它们等待某些事情,比如固定的时间、帧的结束,甚至是另一个协程。无论如何,在yield之后,它们期望得到一个返回值。与yield一起使用的常见函数有:-
WaitForEndOfFrame(): 等待直到下一帧(官方文档:docs.unity3d.com/ScriptReference/WaitForEndOfFrame.html) -
WaitForSeconds(): 等待指定的时间(以秒为单位)作为参数(官方文档:docs.unity3d.com/ScriptReference/WaitForSeconds.html) -
WaitUntil(): 等待直到满足某个条件(官方文档:docs.unity3d.com/ScriptReference/WaitUntil.html)
-
此外,你甚至可以实现自定义的yield指令,如官方文档中所示:docs.unity3d.com/ScriptReference/CustomYieldInstruction.html
注意
对于最好奇的你们来说,协程并不是线程。实际上,协程是在与游戏其余部分相同的线程上运行的。
适应它们需要时间,因为当你有复杂的环境时,它们很难工作,因此它们通常被认为是一个高级主题。但它们解锁了许多可以做的事情的潜力,这对于良好的游戏编程是基本的。不幸的是,在这本书中,我们没有足够的空间为它们分配适当的空间,但我希望随着官方文档、这个小解释以及下一节中生成系统的示例,你将能够更好地理解协程。
绘制其工作原理的想法
我们将把游戏分为波浪。每个波浪都有确定数量的 Panda,这些 Panda 将在一段时间内以递增的强度生成。一旦该波浪的所有 Panda 都被击落,游戏将增加下一波生成的 Panda 数量并开始它。当玩家完成所有波浪后,该关卡可以被认为是胜利。
特别是,我们将在协程中有一个循环来管理不同的波浪,并在开始另一个波浪之前等待波浪结束。第二个例程将负责单个波浪,为其生成 Panda,并检查玩家是否击落了所有 Panda。
设置生成系统
设计师应该有一种方式来放置 Panda 将被生成的位置。因此,我们可以创建一个空的游戏对象,并称其为SpawningPoint。此外,你可以将其附加到一个与上一章中使用的航点类似的 gizmo。结果,它将在场景视图中可见。所以,最后你应该有如下所示的内容:

打开GameManagerScript并添加一个变量来跟踪这个SpawningPoint的位置。由于我们只需要位置,我们可以直接使用 Transform,而不是整个游戏对象:
*//The Spawning Point transform so to get where the Pandas should be
spawned*
private Transform spawner;
要设置其值,让我们像以下这样更改Start()函数:
void Start () {
*//Get the reference to the Player's health*
playerHealth = FindObjectOfType<HealthBarScript>();
*//Get the reference to the Spawner*
spawner = GameObject.Find("Spawning Spot").transform;
}
此外,我们还需要三个额外的变量。一个是用于 Panda 预制体实例化正确的敌人,另一个是玩家需要面对的波浪数量,最后一个是为每波 Panda 的数量(将在波浪之间增加):
*//The Panda Prefab that should be spawned as enemy*
public GameObject pandaPrefab;
*//The number of waves that the player has to face in this level*
public int numberOfWaves;
*//The number of Pandas that the player as to face per wave.
//It increase when a wave is won.*
public int numberOfPandasPerWave;
保存脚本后,我们必须在检查器中分配变量,如下面的截图所示(请随意更改值以适应你游戏的*衡):

管理波浪
在本节中,我们将实现上述两个协程中的第一个。实际上,这个协程将遍历所有波浪,并调用第二个来处理单个波浪。在波浪之间,生成的敌人数量会增加。如果玩家赢得了所有波浪,那么在胜利模式下将调用GameOver()函数。
因此,打开GameManagerScript,我们可以开始编写以下代码:
*//Coroutine that spawns the different waves of Pandas*
private IEnumerator WavesSpawner() {
* //For each wave*
for(int i = 0; i < numberOfWaves; i++) {
*//Let the PandaSpawner coroutine to handle the single wave. When it
finishes
//also the wave is finished, and so this coroutine can continue.*
yield return PandaSpawner();
*//Increase the number of Pandas that are generated per wave*
numberOfPandasPerWave += 3;
}
* //If the Player won all the waves, call the GameOver function in
"winning" mode*
GameOver(true);
}
如您从代码中看到的,我们调用了PandaSpawner()协程,我们将在下一节中实现它。
单个波次
现在是困难的部分。我们需要编写一个能够处理整个 Pandas 波的协程。因此,让我们一步一步来看,从创建协程开始:
*//Coroutine that spawns the Pandas for a single wave, and waits until
"all the Pandas are in Heaven"*
private IEnumerator PandaSpawner() {
*//Rest of the code*
}
首先要做的是初始化numberOfPandasToDefeat变量,以跟踪玩家迄今为止击败了多少 Pandas。当然,我们将这个数字初始化为波次中将生成的 Pandas 数量:
*//Initialize the number that needs to be defeated for this wave*
numberOfPandasToDefeat = numberOfPandasPerWave;
下一步是循环遍历所有待生成的 Pandas,以逐步生成它们:
*//Progressively spawn Pandas*
for(int i=0; i < numberOfPandasPerWave; i++) {
*//Rest of the code inside the cycle*
}
*//Rest of the code outside the cycle*
在循环内部,我们首先需要在生成位置生成 Pandas(没有旋转,这意味着具有作为四元数的恒等性)。然后,我们需要等待一个时间,这个时间取决于剩余的 Pandas 数量和一个随机数。具体来说,我们将计算剩余 Pandas 的数量比率,并使用它来在两个时间之间进行插值。因此,生成的 Pandas 数量越多,等待的时间就越少。然后,我们将这个时间添加到一个随机数上,以在我们的游戏中增加一些随机性。以下是代码:
*//Spawn/Instantiate a Panda at the Spawner position*
Instantiate(pandaPrefab, spawner.position, Quaternion.identity);
*//Wait a time that depends both on how many Pandas are left to be
//spawned and by a random number*
float ratio = (i * 1f) / (numberOfPandasPerWave - 1);
float timeToWait = Mathf.Lerp(3f, 5f, ratio) + Random.Range(0f, 2f);
yield return new WaitForSeconds(timeToWait);
注意
当然,这并不是实现它的唯一方法,代码中的数字是任意的。在真正的游戏中,所有东西都应该通过设计和游戏测试的辛勤工作来决定,以*衡游戏。你可以在下一章中找到更多关于这方面的信息。
在循环之外,相反,我们需要等待所有 Pandas 都被玩家击落(或者满足某些游戏结束条件)之后,才能结束协程,并将控制权交还给WavesSpawner()协程以生成下一波:
*//Once all the Pandas are spawned, wait until all of them are defeated
//by the player (or a gameover condition occurred before)*
yield return new WaitUntil(() => numberOfPandasToDefeat <= 0);
保存脚本,结果,玩家不得不面对许多可怕、甜食成瘾的 Pandas 波次!
主菜单
在许多游戏中,游戏开始时都有一个主菜单,因此,在我们的游戏中,我们也不能忘记主菜单。这将给我们探索更多我们在第一章中提到的内容的机会,Unity 中的*面世界,关于在 Unity 中更改场景。
设计主菜单
如我们在第三章中学习的那样,与玩家通信 – 用户界面,有一个用户界面设计是很好的实践,而主菜单是用户界面的一部分。因此,它应该按照相同的 UI 设计原则来设计。
我们游戏的主菜单非常简单:我们有一个酷炫的背景,屏幕中心下方放置了三个按钮。它们分别是:
-
新游戏:为玩家创建一个新游戏,通过加载我们迄今为止创建的水*
-
设置:触发设置屏幕,玩家可以操作一些选项(这部分留作练习,在作业部分)
-
退出:正如其名所示,它将关闭游戏
因此,我们的设计看起来可能如下所示:

在另一个场景中创建主菜单
要在 Unity 中创建另一个场景,你可以从顶部菜单栏中选择文件 | 新建场景,但最好在场景文件夹内的项目面板中进行导航,这样通过右键单击,你可以选择创建 | 场景。通过第二种方式,场景将直接在正确的文件夹中创建;因此,你的项目是有序和整洁的。
你可以将场景命名为主菜单,然后双击打开它。然后,从头开始,这里有一个空的空间,你可以用你的创造力和想象力来填充!
现在,你应该具备以下技能,而无需逐步解释:
-
创建一个 UI 图像(这将自动生成一个画布以及事件系统),并将其命名为
背景。然后,将其扩展到整个屏幕,并放置你心中的酷炫背景。 -
如果需要达到你心中的效果,请调整画布设置。
-
创建三个按钮,如果你想,可以更改它们的图形和文本,以匹配新游戏、设置和退出。按照上一节的设计放置它们。
-
在一个空的游戏对象中创建脚本,用于处理所有不同的交互。
-
在三个按钮上添加一个
OnClick()事件,并将新创建的空对象拖动到object变量中
一旦菜单创建完成,我们就可以保存场景。
由于我们有两个场景,如果我们想在游戏的最终版本中包含它们,我们需要将它们包含在构建中的场景中。为此,我们需要通过点击顶部菜单栏中的文件 |构建设置…来打开构建设置。你可以从项目面板中将场景拖放到构建中的场景区域,它们将按照确定的顺序出现在那里。你看到的场景旁边的数字是场景的标识符。例如,我们可以使用这个标识符来指定要加载哪个场景。
在我们的情况下,确保主菜单场景在Level_01之前,如图所示:

现在,是时候创建具有所有功能的脚本了。
通过脚本加载场景
创建一个新的脚本,命名为MainMenuFunctionalities。由于它的函数将由OnClick()事件触发,我们需要将它们设置为公共的。
特别是,我们有一个用于加载游戏级别的函数。如果你记得,它的 ID 是1。在 Unity 中加载场景,你使用一个特殊的类,称为SceneManager。因此,我们需要通过在脚本开头添加以下代码行来导入其库:
using UnityEngine.SceneManagement;
注意
SceneManager类以及UnityEngine.SceneManagement库在 Unity 中相对较新。实际上,这些库允许你在运行时对场景执行许多操作,例如一起加载它们、动态加载它们以及卸载它们。这为你提供了一个全新的可能性世界,我希望你有机会去探索,因为在这本书中我们没有时间详细讲解所有内容。无论如何,通常一个好的起点是官方文档,你可以在以下位置找到它:docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.html。
对于那些好奇心旺盛的人来说,在SceneManager类之前,场景是由Application类处理的。所以,如果你有一些过时的代码,仍然使用Application类来加载场景,你知道它是为 Unity 的早期版本编写的。如果这些代码属于你的项目,考虑(如果可能,由于法律问题)使用SceneManager类来更新它。
SceneManager类最常用的功能是LoadScene(),它可以加载另一个场景。指定场景的一种方式是使用其标识符(正如我们将在脚本中做的那样),但还有其他方式,例如使用包含场景名称的字符串。
我们可以以下这种方式实现将被新游戏按钮调用的函数,这非常简单直接:
*//Function that loads the first level*
public void NewGame() {
SceneManager.LoadScene(1);
}
与之相关的设置按钮的功能留作练习(见作业部分):
*//Function that displays the settings*
public void Settings() {
* //Your own code here*
}
最后,退出游戏的功能使用的是Application类(关于这个类的更多信息可以在官方文档中找到:docs.unity3d.com/ScriptReference/Application.html),其中有一个特定的函数可以退出你的游戏:
*//Function that closes the game*
public void Quit() {
Application.Quit();
}
注意
请记住,这个功能在某些情况下可能不起作用,例如当游戏在编辑器中运行(在 Unity 本身中)时,或者例如对于基于网页的游戏。因此,关闭应该在不同*台上发布的游戏可能需要更多的工作。关于多*台游戏的内容将在下一章中详细介绍。
保存脚本,然后回到三个按钮的OnClick()事件上。
为每个按钮分配正确的功能。以下是如何使新游戏按钮事件看起来像的示例:

此外,主菜单已经实现。有了这个,我们的游戏基本上已经完成并可以运行。让我们在下节回顾一下到目前为止我们所做和学到的内容。
本章学到的技术
如果你已经到达了本章和本书的这一部分,这意味着你的游戏已经完成。让我们回顾一下在技术方面的学习内容,而不是主题:
-
继承:我们通过使用继承来实现我们的交易系统,这给了我们探索它的可能性。特别是,我们了解了一些关于以下内容的知识:
-
抽象类和方法:以便它们的完整实现留给子类
-
受保护的变量:某些脚本可以看到,但并非所有脚本都可以看到
-
UI 处理器:可以自动链接以与 UI 进行交互,而无需在检查器中设置事件
-
-
鼠标和相机之间的交互:为了实现放置脚本,我们需要将鼠标坐标转换为游戏坐标。
-
启用/禁用脚本:为了实现功能,始终在放置脚本中,我们学习了如何禁用和启用脚本以在需要时触发功能。
-
存储信息:在
游戏管理器中,我们学习了其他脚本如何访问它们。在整个章节中,我们都这样做,并且以不同的方式。特别是,我们使用了游戏管理器上的公共函数,这些函数被其他脚本调用。因此,游戏管理器成为交换游戏不同部分数据的中枢。 -
使用静态函数:为了在交易系统中再次分配通用变量,我们实现了一个静态函数来设置活动塔。因此,任何脚本都可以访问该函数,而无需获取特定交易类实例的引用(而且父类是抽象的,所以没有实例)。这本来可以没有太多问题完成,因为分配的变量已经是静态的,并且在所有交易类实例之间共享。
-
实现协程:在实现生成系统时,我们使用了协程。这些是特殊的函数,它们有可能被中断并在游戏的其它帧中继续执行。这是我们在这个章节中看到的最强大的工具,尽管它需要比其他工具更多的实践来掌握,但它绝对值得。
-
使用 UI 事件:为了实现函数,在我们的主菜单中,我们通过按钮的
OnClick()事件触发一个脚本中的函数。这样,你可以避免使用 UI 处理器。这种方法的优势在于你可以将所有函数放在一个单独的脚本中,并有一个特定的脚本实例来触发(如果脚本可以被实例化)。另一方面,缺点是需要在检查器中手动链接事件,工作量很大。相反,UI 处理器具有相反的优势和劣势。因此,UI 处理器适合大型脚本,其中包含许多在脚本中实现的功能,并且需要与 UI 进行一些交互。对于小型函数,最好将它们全部放在一个单独的脚本中,并为每个函数创建一个不同的脚本。在任何情况下,最好的解决方案取决于具体情况和你的目标。 -
使用碰撞器来识别区域:我们在放置纸杯蛋糕塔时,使用物理引擎检测鼠标是否悬停在允许放置的特定区域上。此外,我们在纸杯蛋糕塔上使用了一个碰撞器来检测点击(以便被选中)并避免在其他纸杯蛋糕塔上放置其他纸杯蛋糕塔。这些都是使用物理引擎进行非物理相关计算的方式之一。
我希望你在本章中学到了很多,并且已经掌握了我们使用过的不同技术的每个基本概念。为了提高游戏和你的技能,我邀请你完成以下章节中的练习。
作业
在本章中,我们介绍了许多如何在游戏的不同部分之间交换信息的技术,并学习了一些关于游戏编程的知识。这里有几个练习来提高你的技能,并成为一名更好的游戏开发者:
-
甜蜜的资本:当游戏开始时,熊猫们开始出现,玩家应该购买一些纸杯蛋糕塔来保卫他的/她的蛋糕。但是,在最开始的时候,玩家没有任何糖来购买塔,也不能杀死一些熊猫来获得一些糖。因此,在
游戏管理器中添加一个初始糖量变量(以便可以从检查器中设置),并在Start()函数中将这个数量设置在糖表中。结果,玩家将立即准备好与熊猫战斗。 -
暴风雨前的宁静:在这个阶段,当游戏开始时,熊猫们会立即来吃玩家美味的蛋糕。然而,玩家应该有足够的时间在游戏开始时购买并放置一些纸杯蛋糕塔,资金是从之前的练习中设置的。在
wavesSpawner()协程中,在每一波之前设置一个计时器,以便玩家有时间进行评估。然后,在检查器中暴露正确的变量,以便根据级别调整计时器。作为一个变体,你可以在波之间增加或减少这样的计时器。 -
波浪奖励(第一部分):如果你计划在波浪之间显著增加生成的熊猫数量,那么在波浪完成后,你应该考虑用一些糖果奖励玩家。修改
wavesSpawner()协程,为玩家添加一个甜蜜的奖励。然后,在检查器中公开正确的变量以调整每个等级的奖励。 -
波浪奖励(第二部分):在完成前面的练习后,制作一个根据波浪数量变化的奖励数组。然后,在每个波浪结束时,为玩家分配正确的奖励,以便能够调整奖励,不仅针对每个等级,而且针对每个波浪。
-
单例模式(第一部分):在我们的游戏中,有一些脚本应该只有一个实例,例如
游戏管理器、生命条或糖量计。因此,最好使它们唯一,因为我们的某些脚本依赖于这样的类只有一个实例的隐含(但未保证)事实。因此,你应该实现一个名为单例的模式。你当然可以在互联网上搜索如何实现它,但尽量提出你自己的解决方案。许多在线实现依赖于静态变量来检索类的单个实例。由于我们的脚本将使用FindObjectOfType()函数找到这些类,你可以尝试探索其他方法。所以,尝试为这个问题提供你的解决方案,并为GameMangerScript、HealthBarScript和SugarMeterScript实现它。 -
单例模式(第二部分):在第一部分之后,你应该已经以你的方式实现了单例模式。现在,查看以下两个链接:
wiki.unity3d.com/index.php/Singleton和unity3d.com/learn/tutorials/projects/2d-roguelike-tutorial/writing-game-manager,因为它们都实现了单例模式。将它们与你提出的进行比较,并为每种方法突出显示其优点和缺点。你认为哪种方法在我们的游戏中会更好?这种方法对于游戏管理器、生命条或糖量计是否有所不同?为我们的塔防游戏实现你认为值得的单例模式。 -
改进允许区域(第一部分):我们已经看到如何使用碰撞器来检查鼠标是否悬停在允许的区域上,这样放置脚本就知道在需要释放蛋糕塔时,它是否是一个合适的位置。但在
游戏管理器中会发生什么呢?即使没有塔需要放置,它仍然会检查允许的区域并更新其内部状态。考虑一个解决方案,其中游戏管理器仅在放置脚本请求时检查鼠标是否悬停在允许的区域上。因此,你的新解决方案应该提高游戏管理器的性能。 -
改进允许区域(第二部分):这项练习与第一部分是独立的。在允许区域系统中,我们只考虑了鼠标。如果你想在移动*台上导出游戏,比如在 Android 设备上,会怎样呢?在这种情况下,是否应该完全重新设计或更改允许区域系统?因此,设计并实现一个适用于尽可能多*台的系统。
-
改进允许区域(第三部分):这项练习与第一部分和第二部分是独立的。我们提出的允许区域系统对于多级游戏(很可能你就有这样的游戏)来说并不容易使用,因为你不能在
游戏管理器预制件中放置碰撞器,因为它们依赖于特定的级别。你能想到一个更简单的解决方案,让关卡设计师能够逐级告诉游戏管理器哪些区域是允许的吗?一旦你设计了这样的系统,就在我们的塔防游戏中实现它。 -
改进允许区域(第四部分):考虑你在第一部分、第二部分和第三部分中找到的所有针对不同问题的解决方案。尝试将它们合并成一个针对允许区域的终极解决方案。目标是创建一个系统,它从计算角度来看是高效的,对于游戏和关卡设计师来说易于使用,并且是多*台的(以便能够在多个*台上部署游戏)。
-
玩家反馈(第一部分):这是一系列相互独立的练习,目的是提高游戏提供给玩家的反馈,这对于游戏具有吸引力至关重要。当玩家进行交易时,他/她出售、购买或升级塔,但没有反馈表明操作成功。因此,你需要实现一些视觉反馈。以下是一些更小的练习:
-
当从
糖度计中减去或添加糖分时,添加一个动画,以便在糖度计上显示一个大的数字,显示变化的数量。此外,考虑根据数量以及是添加还是减去来改变这个数字的颜色。 -
当糖分从
糖度计中减去或添加时,添加一个动画来显示糖度计上的数字变化,而不是突然改变显示的数字。 -
当塔升级时,考虑在塔上播放一个动画。同样,当塔被出售或放置(在购买后)时也是如此。
-
-
玩家反馈(第二部分):这是一系列相互独立的练习,目的是提高游戏提供给玩家的反馈,这对于游戏吸引玩家至关重要。当玩家进行交易时,他/她出售、购买或升级塔,但没有关于这些操作将做什么/改变什么的信息,例如:购买一个塔的价格是多少?因此,你需要实现一些视觉反馈。以下是一些较小的练习:
-
当玩家悬停在交易按钮之一上时,将价格(或出售按钮的情况下的价值)显示在某个地方(需要仔细决定,因为它会影响我们在第三章中做的设计),这样玩家在执行操作之前可以阅读它。
-
当没有选择任何塔时,销售和升级按钮都不应该显示为激活状态。将其更改为,当
currentActiveTower变量为 null 时,显示一个禁用按钮。
-
-
实现设置菜单(第一部分):在这一章中,我们将这留作练习,所以让我们看看我们需要做什么。首先,要决定玩家可以更改哪些设置以及如何更改(切换?滑块?下拉菜单?)。特别是,你应该至少有一个音频切换和一个质量设置下拉菜单,以及你想要包含的任何选项。然后,完成 UI 的完整设计。最后,在 Unity 中创建一个新的场景(或屏幕,根据你的喜好),通过使用 UI 元素实现设置屏幕。
-
实现设置菜单(第二部分):在第一部分中,我们进行了设计和在 Unity 中实现了它。现在,我们需要实现功能(目前不包括音频,留到下一章),所以创建一个脚本,类似于我们在主菜单中做的,在那里实现所有功能,并通过使用检查器中的事件将它们链接到 UI 元素。要修改质量设置和音频设置,搜索官方文档了解如何操作(这是练习的一部分)。此外,请记住,下一章可能会给你一些其他实现设置类型的思想。
-
魔法数字(第一部分):我们在前面的章节中已经遇到了魔法数字。它们是在脚本中出现的没有解释的数字,良好的实践是尽可能避免它们。在本章中,我们也留下了许多这样的数字;让我们尝试去除它们。第一个魔法数字是在创建我们塔的位置的新向量时放置脚本中的数字7。这个数字取决于摄像机的位置以及塔应该在z轴上的位置。因此,添加一些代码来以动态方式计算这个数字(这样如果我们决定改变摄像机的位置或塔的z深度,我们可以在不改变脚本的情况下做到这一点,作为额外的奖励,你还将有机会在不同的z深度层上有不同类型的塔,这对你来说可能同样有用)。特别是,你需要从塔的z轴减去摄像机的z轴。
-
魔法数字(第二部分):在
CupcakeTowerScript的Upgrade()函数中,我们也留下了一些魔法数字。创建可以在检查器中设置的变量,以去除任何剩余的魔法数字(例如增加销售价值或升级成本)。
摘要
在本章中,我们探讨了在脚本之间交换信息和数据的技术。通过这样做,我们完成了我们的塔防游戏的实现。
熊猫走向玩家的蛋糕去吃它,纸杯塔向它们射击,因此熊猫死亡,它们也会定期生成。玩家可以购买、出售和升级纸杯塔。有一个主菜单,玩家可以赢或输。所以,我们的游戏完成了。是吗?我们能更进一步吗?让我们在下一章中找出答案。
第八章. 蛋糕之外是什么?
"从五彩缤纷的纸杯蛋糕到美味的甜点,发现如何挑逗消费者的味蕾!"
在本章的第一部分,我们将展示一系列关于如何改进你的游戏的想法。其中一些部分在之前的章节中已经有所预示。为了详细解释这些部分,并展示代码,我们需要更多的时间和空间,但我们很遗憾没有这些资源。因此,你需要将这些部分视为练习。实际上,这是唯一一章中没有“作业”部分(尽管有一些明确的练习)。请随意扩展并实现你最感兴趣的部分。毕竟,没有什么能比经验、试错更能教会你了!
在本章的后面部分,我们将发现蛋糕之外的东西。事实上,游戏开发不仅限于游戏本身;还有许多围绕它构建的东西你应该考虑。这些事情可能包括团队合作、测试、营销和本地化!当然,这不是一本关于视频游戏营销的书,但它为你提供了开始超越游戏本身的起点!
最后,我必须说,你应该为自己感到骄傲,因为你已经走到了这一步!事实上,你不仅完成了一个塔防游戏,还学到了很多关于 Unity 的知识,而且你还在这里,站在最后一章!所以,为了保持你的动力,在继续阅读最后一章之前,去拿一块蛋糕吧。
增强和改进你的游戏
本节的目标是给你一个了解你游戏潜力的概念,以及一个努力的方向。毕竟,如果你已经走到这一步,你也将能够自己走路,所以我不必详细解释每一件事。
改进纸杯蛋糕塔
在这里,我们将关注如何通过探索一些想法和新方向来改进我们的纸杯蛋糕塔,例如射击策略或一种特殊的糖霜。
射击策略
回到第二章,烘焙纸杯蛋糕塔,我们实现了第一个射击塔附*熊猫的纸杯蛋糕塔。然而,这并不是你可以选择的唯一策略。实际上,你也可以允许玩家选择一个更适合他们策略的选项。
这些策略可能包括:
-
射击弱者/强者熊猫
-
射击健康值较低/较高的熊猫
-
射击范围内最远/最*的熊猫
-
射击第一个/最后一个进入范围的熊猫
随意添加更多并实现你自己的想法。
小贴士
如何实现这些:
在CupcakeTowerScript中,我们遍历所有的熊猫(实际上是碰撞体,但我们通过标签过滤它们)。我们找到了并计算了每个熊猫的距离。一些概念可以应用于前面的列表。在距离最远的熊猫的情况下,这是立即的,因为你可以有一个max变量,而不是min变量。同样,对于更弱/更强和更少/更多健康的熊猫,情况也是如此,你仍然需要一个min或max变量,但不是距离,而是从PandaScript中获取一些属性。在搜索第一个或最后一个进入范围的熊猫的情况下,情况要复杂一些。实际上,你需要有一个熊猫进入和退出范围的数据结构,然后从这个结构中检索要射击的熊猫。
特殊洒水器
在我们的游戏中,所有的洒水器都只有伤害和速度,这些可以由发射它们的蛋糕塔设置。然而,你可以有更多类型的洒水器,其中一些可以具有特殊效果。
这些效果的例子可能包括以下内容:
-
冻结:在一定时间内减缓敌人的概率。
-
毒药:有几率使熊猫中毒;这会随着时间的推移减少其健康值(通常在短时间内)。
-
爆炸:它们不仅会伤害它们击中的熊猫,还会伤害周围的熊猫。
-
暴击:即使熊猫剩余生命值不多,洒水器也有可能杀死熊猫的概率。但例如,这并不适用于 Boss,在暴击中它们只会受到双倍伤害。
随意添加您自己的内容。
小贴士
如何实现这些:
你可以通过使用继承从ProjectileScript类派生来创建特殊的洒水器类。这个派生类可以包含有关它们效果的一些额外数据。在PandaScript中,你可以在OnTriggerEnter2D()函数中检索熊猫被哪种洒水器击中,以便检索其信息并对其熊猫应用效果。
在爆炸效果的情况下,被击中的熊猫应该使用Physics2D.OverlapCircleAll()函数(如在CupcakeTowerScript中)来找到附*的熊猫。
此外,你可以给它们添加动画,使它们有很好的*滑动画。甚至只是一个旋转动画看起来都很棒。
老化和定价模型
谁说一座塔不能变老?鉴于蛋糕塔自其诞生以来一直在不断向熊猫射击,它可能会受到高利贷的影响。因此,其性能可能会随时间下降,如果玩家尝试出售它,其价值也可能下降。
你可以添加另一个交易选项来修复蛋糕塔,使其恢复正常工作。
小贴士
如何实现这个:
您可以在CupcakeTowerScript中实现一个协程,在固定的时间后,通过降低其统计数据来老化塔楼,最终直到它停止工作。如果您还想要实现修复功能,您需要将塔楼的原始值存储在某个地方,以便在修复选项之后恢复它们。
除了老化,还有哪些随时间动态变化的各个价格和成本?理想情况下,您可能希望为这些创建一个真正的定价模型(这需要大量的设计工作),但一旦它起作用,将会非常受欢迎。实际上,这是您在*衡游戏时需要考虑的事情之一。
小贴士
如何实现:
再次,您可以在CupcakeTowerScript中创建一个协程,根据塔楼(以及可能的游戏)的当前状态改变其成本。因此,当交易系统检索这些值时,它们将根据您的模型价格进行更新。
改进用户界面
此外,我们游戏的用户界面可以改进,例如,一个通知告诉玩家他/她还有多少波要击败。添加这类信息可能非常有用。下一步是确定在哪里?这是一个需要考虑的设计选择,因为它可能会影响我们在第三章中设计当前 UI 时达到的*衡,与玩家沟通——用户界面。
因此,一个好的练习就是通过考虑尽可能多的因素(包括在交易系统中显示价格和成本;在上一章中有一个关于这个的练习)来迭代整个游戏界面的设计。
注意
一本关于如何设计用户界面的非常好的书是 Jeff Johnson 的《Designing with the Mind in Mind》,但这只是众多书籍中的一本,所以请确保查看其他关于游戏 UI 的更具体书籍。
提高关卡质量
在这里,我们将关注我们可以做的事情来提高我们的关卡。至于上一节中的杯子塔,我们将探索新的想法和方向,例如增加更多关卡、敌人和要遵循的路径。
多级
当然,您的游戏必须包含多个关卡!我们只实现了一个关卡,但您绝对应该将您的游戏扩展到多个关卡。
在这个阶段,您能够创建自己的地图和关卡。但这里有一些提示和考虑因素,您可能会觉得有用:
-
记住静态变量是持久的。因此,当您更改场景时,它们需要用正确的值重新分配。
-
您可以通过使用
DontDestroyOnLoad()函数(当您需要实现音乐时很有用;我们将在音频部分看到这一点)在场景之间使一些对象持久。 -
您必须做出设计选择,例如:玩家收集的糖在关卡之间是否保留?以及他的/她的健康状态?
大型地图
我们可以在游戏中实现非常大的地图,大到无法一次性在屏幕上显示。这样,我们需要实现一种方式,让用户可以移动摄像头来移动关卡(或者你也可以实现摄像头固定,其余部分移动)。
小贴士
如何实现:
附着在摄像头上的脚本可以检测玩家拖动鼠标时的情况,以便将摄像头移动到相反方向(向左拖动,摄像头向右移动)。
许多路径
没有什么能阻止我们创建一个地图,其中熊猫在某个点的路径会分叉或与其他路径合并。我们甚至可以有更多的繁殖点,或者有超过一种进入蛋糕的方式。
所有这些都可以通过我们的航点系统来处理。实际上,熊猫会调用GetNextWaypoint()函数,该函数可以返回地图中的任何航点。
因此,你可以创建许多不同的航点,它们继承自航点的父类。在分叉的情况下,你可以随机选择将熊猫发送到哪个地方。以下是一个代码片段,以供参考,当路径在两点之间分叉时:
[SerializeField]
private Waypoint waypoint1, waypoint2;
public override Waypoint GetNextWaypoint() {
if(Random.Range(0,2) == 0) {
return waypoint1;
}else {
return waypoint2;
}
}
但你真的可以实现你心中的任何想法,只要你的思考是基于航点的!你还可以让GetNextWaypoint()接受一些参数,比如熊猫本身,然后根据熊猫决定将熊猫发送到哪个航点!基本上,可能性很多,将航点作为单个实体来构建的结构允许我们这样做。
许多熊猫
我们不能只限于一种类型的熊猫!我们还可以制作绿色、红色和紫色的熊猫!每个都有不同的统计数据。例如,有些真的很慢,有很多健康值,并且会吃掉很多蛋糕。其他人真的很快,健康值低,当它们到达时,只会吃掉玩家蛋糕的一小部分。
你甚至可以考虑给他们不同的能力,并且再次你可以使用继承的概念来实现从PandaScript派生的其他熊猫类。然而,请记住,你还需要考虑改变繁殖系统!
多阶段 Boss
在不同种类的熊猫中,你可以在关卡结束时添加一个 Boss,用来击败头上戴着王冠的巨型熊猫:

Boss 的不同阶段可以用有限状态机来构建,不同的转换由其健康量或它离玩家蛋糕的远*触发。它也可能有变得非常愤怒并偏离轨道去吃一个纸杯蛋糕塔的可能性!
一个更好的繁殖系统
由于我们添加了这么多不同种类的熊猫,我们需要重新构建我们的繁殖系统。理想情况下,你希望找到一个解决方案,让设计师更容易在检查器中展示不同波次的敌人,以及每种类型的敌人数量,就像这张图片一样:

你可能想知道是否真的可以在 Unity 检查器中做到这一点。答案是肯定的;实际上,Unity 编辑器可以被扩展(我们将在本章后面看到)。
运行时切换难度
另一种可能使游戏适应更广泛受众的绝佳方式可能是根据玩家的表现运行时切换游戏的难度。实际上,你可能希望通过提高难度级别来保持寻求挑战的玩家参与,如果他们在游戏中相对容易地前进,或者帮助那些遇到困难的玩家。
有不同的方法可以做到这一点。最简单的一种包括基于玩家分数的 if 链,以增加或降低难度;而最复杂的一种包括自适应学习算法。
小贴士
如何实现:
无论你的方式是什么,首先要做的是将你的整个游戏链接到一个难度参数,可能是在游戏管理器中。每当这个参数改变时,所有依赖于它的游戏部分都会改变(参考关于脚本间通信的更多内容部分)。然后在另一个脚本中,你可以实现你的自适应算法,该算法通过接收游戏的不同状态,可以确定是否增加或降低难度。
训练和扩展你的 Unity 技能,成为一名更好的游戏开发者
在本节中,我们将提供一些关于如何提高你的 Unity 技能和整体改进任何游戏(包括这个塔防游戏)的想法。
让其他团队成员的工作更简单
当你在 Unity 中编写脚本时,你需要记住你的代码将被不同的团队成员使用(我们将在本章后面了解更多关于团队合作的内容)。因此,请记住以下几点:
-
通过文档(我们将在团队合作部分稍后看到)提高你代码的可读性,以便你的程序员同事。
-
在检查器中创建一个友好的界面(也可以通过使用自定义检查器;稍后我们将看到)以便设计师容易使用(一个例子是暴露事件,我们将在下一节中探讨)。
暴露事件
如果你正在编写一个需要与其他游戏元素交互的非常酷的脚本,你实际上可以在你的脚本中暴露你自己的事件。
实际上,你可以在你的代码中添加这个库:
using UnityEngine.Events;
因此,你将能够以下述方式声明和触发事件:
*//Float variable just for test. Can be set in the Inspector.*
public float myFloat;
*//Declaration of the event class. It has a float as parameter to pass.*
[System.Serializable]
public class OnEveryFrame : UnityEvent<float> { }
*//Declare the variable event which will be shown in the Inspector*
public OnEveryFrame OnEveryFrameEvent;
*//Function that is called every frame*
void Update() {
* //fire the event at every frame*
OnEveryFrameEvent.Invoke(myFloat);
}
这就是它在检查器中的样子:

很酷,不是吗?
糖霜池
在 Unity 中实例化一个游戏对象是一个缓慢的操作;因此,每当纸杯塔射击时,就会实例化一个新的糖霜,然后从游戏中移除。
一个更好的解决方案是进行对象池化,这是一种优化技术(我们在第二章中提前提到了这一点,“烘焙纸杯蛋糕塔”)。其概念是在开始时实例化一定数量的糖霜并保持它们在屏幕之外。当一个纸杯蛋糕塔射击糖霜时,它只是从这个池中取出一个糖霜并射击。当糖霜应该被销毁时,相反,它被移回到屏幕外的池中,以便另一个塔楼可以再次拾取。
在存在许多塔楼射击的场景中,这可以显著提高你的游戏性能,尤其是如果你针对的是移动*台。
保存你的数据
我们还没有讨论保存游戏数据(遗憾的是我们无法在这本书中涵盖所有内容)。然而,Unity 提供了一个名为PlayerPrefs的类(官方文档:docs.unity3d.com/ScriptReference/PlayerPrefs.html),它非常适合在游戏会话之间保存一些值,例如分数。
如果你需要保存很多东西,你应该通过保存自己的文件来实现自己的解决方案。文件使用什么格式由你决定。Unity 从许多版本开始就内置了对 XML 的支持,最* Unity 也增加了对 JSON 文件的内建支持。
最后,你还应该考虑将数据保存到云端,例如在线数据库。
调试
错误修复很棘手,但你有一个非常强大的盟友:控制台。不要低估这个工具的价值。这是因为它将帮助你摆脱许多困境,尽管一开始它所记录的内容可能看起来没有意义,但随着时间的推移,你会学会如何解释这些消息并快速纠正错误。
另一种使用控制台的好方法是插入调试日志。在 Unity 中,它们可以通过Debug类打印出来。以下是一个示例:
Debug.Log("This is a string that will be printed on the console");
将此行代码放置在战略位置可以帮助你识别错误并最终纠正它。
远程日志
如果你的目标是其他*台,Unity 提供了使用你的强大盟友(控制台)远程操作的可能性。例如,对于 Android 设备,你可以阅读这篇文档(它实际上允许你不仅通过 ADB 连接使用控制台,还可以使用调试器):
docs.unity3d.com/Manual/AttachingMonoDevelopDebuggerToAnAndroidDevice.html
否则,你可以实现自己的解决方案来显示控制台日志或查看其他人的代码。例如,你可能想看看这个免费资源:
www.assetstore.unity3d.com/en/#!/content/12294
清理发布版本
但是,你应该记得从最终版本中删除这些代码行;否则,它们会消耗你本可以用在其他方式上的计算能力。
处理调试语句的一个好方法是创建另一个类来打印调试信息。如果你包含了Diagnostics库,你可以定义一个条件属性,如下面的类:
using UnityEngine;
using System.Diagnostics; *//Needed for the Conditional attribute*
public class myDebug : MonoBehaviour {
[Conditional("DEBUG_MODE")] *// Conditional attribute*
public static void Log(string message) {
* //Print the message in the console*
UnityEngine.Debug.Log(message);
}
}
因此,你可以使用以下代码行,而不是之前的那个:
myDebug.Log("Hello World");
只有当你的游戏中定义了DEBUG_MODE属性时,这一行才会被编译。你可以通过导航到编辑 | 项目设置 | 玩家来检查你的游戏中存在哪些属性。在下面的屏幕截图中,你会找到定义此类属性/符号的位置,如图所示,我已经添加了DEBUG_MODE属性(你应该在发布游戏之前将其删除):

小贴士
对于这个系统的非常棒的实现,你可以查看这个链接中的代码:gist.github.com/kimsama/4123043。
更多关于脚本间通信的内容
在上一章中,我们看到了脚本之间可以相互通信的许多方式。然而,Unity 提供了许多其他方式。其中之一是通过消息来触发函数。实际上,你可以向一个游戏对象发送消息,要求触发某个函数,如果该函数存在于该游戏对象附加的任何脚本中。
然而,这是一种从计算角度来看代价高昂的脚本间通信方法,并且只有在必要时才应该使用。
此外,Unity 提供了不止一个消息系统,每个系统都有其优点和缺点。
在任何情况下,这些系统都可以用来创建你自己的消息系统,这是一个在特定事件发生时向许多脚本广播消息的绝佳方式,例如,当你需要在运行时切换难度,许多脚本都应该知道这个变化时(参考运行时切换难度部分,获取更多信息)*。你可以在网上找到许多实现,例如wiki.unity3d.com/index.php/Advanced_CSharp_Messenger,但我强烈建议你创建自己的,因为这样你一定会学到更多。例如,你可以遵循这个教程:unity3d.com/learn/tutorials/topics/scripting/events-creating-simple-messaging-system。
代码文档化
当你在团队中工作时,你需要适当地注释你写的所有代码。有一些工具可以帮助你自动生成文档,但你需要付出努力来注释每一件事!
使用摘要标签可能也是值得的,我在创建塔防项目时没有使用它,以提高本书代码的可读性:msdn.microsoft.com/library/2d6dt3kf.aspx。
在任何情况下,当您编写代码时,您应该始终在性能和可读性之间保持*衡,这两者往往相互矛盾。如果文档丰富且充分,即使是最复杂的性能优化,也能使代码变得难以阅读,但仍能理解。请记住:在编写代码时保持一定的优雅性应该是您的指导原则。
保护您的游戏
一旦您拥有您的游戏,您可能希望保护它免受盗版和/或游戏破解。关于这一点,有大量的文献,以及关于它的不同意见。事实上,对这些系统的主要批评之一是它们使黑客和合法玩家的生活变得更加困难;游戏最终会被破解,您最终会面临不满的玩家。另一方面,相反的观点认为,从游戏产生的收入的大部分发生在发布日期的第一个月(我们谈论的是非在线游戏),因此持续一个月的保护系统已经完成了它的任务。
注意
在玩家和黑客中都非常受欢迎的(用于游戏的)反盗版系统之一是 Denuvo (www.denuvo.com)。尽管 Denuvo 收到了很多批评,但它阻止了许多游戏立即被破解,并为破解其他游戏提供了困难。您可以在维基百科上找到使用此系统保护的游戏列表(无论它们是否已被破解):en.wikipedia.org/wiki/Denuvo。
对于最好奇的您,Denuvo 是一种反篡改系统,而不是数字版权管理(DRM)系统(它将游戏合法副本绑定到用户)。反篡改使得逆向工程 DRM 系统以绕过它变得更加困难。
然而,如果您决定为您的游戏包含某种保护措施,但没有时间开发自己的系统,您可以从资产商店购买价格低廉且有用的安全工具,例如这些:反作弊工具包 (www.assetstore.unity3d.com/en/#!/content/10395) 或 PlayerPrefs Elite (www.assetstore.unity3d.com/en/#!/content/18920)。
注意
对于更高级的保护系统(例如上述的 Denuvo),当然价格会上升,这使得独立游戏开发者难以负担其中之一。总之,在您将部分预算用于保护系统之前,仔细考虑它是否对您的游戏是必要的。
注意
这里有一个简短、有趣且易于理解的历史概述:www.youtube.com/watch?v=HjEbpMgiL7U。
为多个*台构建
当你考虑在不同*台上发布你的游戏时,你应该记住你将要针对的不同*台。它们有触摸输入吗?它们支持控制器吗?基于此,你需要修改你的应用程序中的代码,以便它可以在多个*台上运行。
Unity 提供了插入一些汇编指令的可能性,以便以某种方式编译代码。例如,你可能希望在为 Android 构建游戏而不是 Windows 时以某种方式编译一小部分代码。以下是文档链接:docs.unity3d.com/Manual/PlatformDependentCompilation.html。
小贴士
在设备上进行快速原型设计时,你应该考虑一些模拟器(我们将在本章后面看到)。然而,Unity 为 Android 设备提供了一个名为 Unity Remote 的应用程序(你可以在 Play Store 中找到它:play.google.com/store/apps/details?id=com.unity3d.genericremote),它允许你通过 USB 连接直接从 Unity 编辑器测试你的游戏。
输入/输出设备
玩家将如何使用你的游戏以及游戏过程中将如何使用?从游戏机到外设,有大量不同的硬件设备可以被你的游戏利用。为了帮助你入门,这里有一个基本列表,当你开发你的想法和*台选择时,你应该记住以下输入/输出设备:
-
Xbox(360、One):
-
Kinect
-
控制器(有线、无线)
-
各种外设(麦克风、DJ 控制站、吉他等)
-
-
PlayStation(1、2、3、4):
-
Move 控制器
-
EyeToy
-
控制器(有线、无线)
-
各种外设(麦克风、DJ 控制站、吉他等)
-
-
Wii:
-
控制器/摇杆
-
运动传感器
-
*衡板
-
各种外设和附加设备(麦克风、DJ 控制站、吉他、方向盘、体育设备等)
-
-
Mac/PC:
-
操作系统(OS X、Windows、Linux)
-
显卡
-
显卡
-
主板
-
声卡
-
网络卡
-
处理器
-
存储空间
-
鼠标
-
键盘
-
屏幕
-
游戏手柄
-
控制器
-
各种其他外设(方向盘)
-
-
便携式设备(手机、*板):
-
安卓
-
苹果
-
Windows
-
-
音频设备:
-
扬声器
-
头戴式耳机
-
-
必要的网络连接:
-
高速互联网
-
局域网连接
-
移动网络
-
Wi-Fi
-
蓝牙
-
-
虚拟现实(VR)头戴式设备(如 Oculus Rift 或 HTC Vive):
-
头部追踪
-
触控控制器
-
Oculus 远程控制(仅适用于 Oculus Rift)
-
相机(仅适用于 HTC Vive)
-
-
Leap motion:
- 手和手势追踪
-
脑电图(EEG)头戴式设备(如 Emotiv 头戴式设备):
- 脑电图(EEG)
Unity 中的虚拟现实
Unity 的最*版本支持虚拟现实设备(如 Oculus Rift、HTC Vive 或 Samsung Gear)。
要开始在 Unity 中开发虚拟现实,你应该首先下载适当的 SDK 并安装它。然后,从顶部菜单栏选择编辑 | 项目设置 | 玩家。在其他设置下,有支持虚拟现实选项。一旦勾选,你可以在选项下面的菜单中检查哪些 SDK 可以用于,如下面的截图所示(在这个例子中,有 Oculus SDK):

*衡游戏
不要忘记*衡游戏。这非常重要,可以决定你游戏的成功与否。为了正确*衡你的游戏,你需要进行大量的游戏测试(我们将在后面看到更多关于这一点的内容)。
想象一下我们正在玩一个合作游戏,我有一根魔法棒,特殊的能力和无敌状态。而你,则有一盏手电筒。除非手电筒是你所拥有的所有东西的紧凑版,不幸的是,它不是,否则这显然是不*衡的。现在,这不仅仅是在两人或多人游戏中是一个问题,在单人游戏中也是如此,AI 非常强大,只是为了击败他们,就会耗尽你的所有资源,如果你能击败他们的话。同样,当你有限的机会获得升级时,而其他玩家显然没有这个问题。
*衡游戏对于许多原因来说都很重要,因为它提供了一个公*的竞技场,并给每个人提供了赢、输和进步的机会。随着你游戏的每一次迭代和每一次后续更新(尤其是如果你改变了某些重要的事情),确保你的游戏是*衡的。对于单人和合作游戏,这将是通过游戏测试来解决的问题。对于大规模的 MMO 游戏,你可以进行游戏测试,但很可能会出现不*衡的问题,随着人们玩得时间越长,探索世界,发展并升级他们的角色,以及在世界中前进,这些问题就会显现出来。
接*游戏*衡的许多方法之一是通过成本分析的方法。在这种方法中,你可以从添加、删除和替换你游戏中不同的功能开始。例如,而不是添加一个新角色,你可以使一个现有的角色变得更强大,或者用更强大的版本替换它。所以,而不是一个特殊巫师,你可以用史诗巫师来替换它。或者,你可以完全删除特殊巫师,或者添加另一种类型,比如元素巫师。在每种情况下,他们的能力都会改变(或被删除),结果这将影响游戏的总体动态,包括其他角色将如何与之战斗。一些现实生活中的例子可以在像Clash of Clans和Clash Royale这样的游戏中找到,这些游戏定期进行调整,以*衡游戏玩法。
注意事项
想要进一步阅读,请查看以下链接:gamedesignconcepts.wordpress.com/2009/08/20/level-16-game-balance.
扩展 Unity 编辑器
Unity 编辑器实际上是可以扩展的。实际上,一些脚本可以使用 Unity 编辑器库来访问所有与编辑器相关的功能。你可以在这些脚本的开始处添加以下代码行来导入它:
using UnityEditor;
这是一个非常强大的工具,因为你可以通过它实现自定义功能,或者为你的脚本提供简单的接口。
这里是官方文档的链接:docs.unity3d.com/Manual/ExtendingTheEditor.html.
然而,由于这是一个非常广泛的话题,我建议你阅读一些博客文章中的教程,或者购买一些特定的书籍,例如,Angelo Tadres 编著的 Extending Unity with Editor Scripting,Packt Publishing 出版,你可以在这里找到它:
www.packtpub.com/game-development/extending-unity-editor-scripting.
多玩家和网络
如果你和你的朋友能一起建造纸杯蛋糕塔,并合作面对成群的甜食爱好者熊猫,那岂不是很有趣?如果其中一个人能控制熊猫,另一个人控制纸杯蛋糕塔,岂不是更酷?如果你能将世界各地的许多玩家连接起来,所有这些场景都是可能的,只需将你的游戏转变为多人游戏,这样就可以通过网络由许多不同的玩家进行游戏。
Unity 提供了许多内置的多玩家游戏功能,但它们并不像你预期的那么容易。但如果你有预算,有许多第三方插件可以使管理多人游戏变得更容易。Unity 的资产商店里充满了这些插件!
在任何情况下,编写多人游戏都不是一件容易的事情,因为有许多不同的部分(如服务器、客户端、安全协议等)应该完美地协同工作,而这并不简单。因此,多玩家和网络是游戏开发中最难面对的话题之一(尽管是可能的,否则你不会看到周围有这么多在线游戏)。
要开始理解多人编程的原理,最好的方法是阅读相关的书籍,并且(如果你使用 Unity 的内置功能)阅读官方文档,你可以在这里找到它:docs.unity3d.com/Manual/UNet.html.
熟能生巧
没有人天生就懂得游戏编程或 Unity。你需要通过努力工作来获得它,而且无论你的经验水*如何,总有新的东西可以学习(正如我们在第一章中提到的,《Unity 中的*坦世界》)。
为了成为一名游戏开发专家,你需要不断练习(就像生活中的每一件事一样),因为每个角落都有东西可以学习。挑战自己,参与项目,给自己布置家庭作业。实际上,在这本书中,我已经为你提供了一些家庭作业,但通常你需要自己成为自己的老师,并强迫自己完成家庭作业来提高你的技能。
在做这件事的时候,不要忘记向那些在我们之前的人学习,他们已经找到了一些在大多数情况下都很有用的方法和技巧。实际上,你应该学习和发展你自己的最佳实践。在 Unity 的情况下,你可以从访问这个链接开始:docs.unity3d.com/Manual/BestPracticeGuides.html。
改善游戏氛围
你是否曾经玩过一款游戏或看过一部电影,感觉就像你就在那里,你感到着迷,沉浸其中,完全成为世界的一部分……那一刻?这一切都与氛围有关。创造正确的情绪需要许多不同的事情,不仅仅是外观,还有声音;它也有一种向玩家传达信息的方式。不幸的是,我们目前还不能利用触觉(尽管有很多关于触觉界面的研究),味觉或嗅觉(除了 Ubisoft 创建的原型设备 Nosulus;可以在nosulusrift.ubisoft.com找到))。因此,它们的外观和声音对于氛围至关重要!
视觉效果
游戏不一定要看起来很棒才能让人沉浸其中,但它们需要提供一个有意义的视觉环境。视觉效果可以从天空盒、物体的美学风格和环境风格中变化。
色彩方案
色彩代表情绪,任何上过艺术课的人都会很快学到,色彩有助于向观众表达情感。尽管对色彩没有唯一的解释,但你可以在整个游戏中坚持使用一种惯例。
开发色彩方案的一个很好的工具(如我们在第三章中提到的,与玩家沟通——用户界面)是 Adobe Color CC,你可以在color.adobe.com找到:

注意
你知道颜色与我们的情感相关的概念起源于一个名叫罗伯特·普鲁奇克*的人,他在 1980 年创建了第一个情感色彩轮吗?
家庭作业
看一看一些游戏。它们是否有一种颜色比其他颜色更突出?如果是这样,如果你将颜色改为相反的颜色会发生什么?例如,如果主要是蓝色,如果你将其改为红色或黄色会发生什么?列出受主导颜色影响的对象。为什么会这样或不会这样?如果它们是/不是,如果你改变它们,这会如何影响氛围?
灯光
当您走进一个充满光线的房间时,可能会感到清新、充满活力和积极。相反,想象一下走进一个没有任何光线可以判断里面有什么,或者有多大空间的黑暗房间;可能会感到有些令人畏惧。这可能是同一个房间,但两种不同的照明条件提供了两种非常不同的氛围。如果再配上其他感官线索,如声音,可能会使这一切更加恐怖或振奋人心。当然,这些都是您的典型环境;一个明亮的房间并不意味着不会有可怕的东西跳出来,就像一个黑暗的房间并不意味着里面充满了敌人。这些都是需要考虑的事情,尤其是在理解如何传达危险或和*感时,尤其是在 3D 环境中。
Unity 中的灯光
Unity 实现了不同类型的灯光,可以在我们的场景中放置。我们没有介绍它们,因为它们与 3D 游戏紧密相关,尽管您也可以将它们用于 UI(因此也可以用于 2D 游戏)。
如前文所述,您需要将场景中的灯光视为一种艺术表达,以便创造氛围。令人惊讶的是,看起来不错的场景根本不需要逼真的灯光。在游戏开发中,许多事情都存在性能上的权衡,因为通常灯光很昂贵,尤其是如果实时计算的话。实际上,最常用的技术之一是“烘焙”灯光,这意味着预先计算它们。当然,这种方法有很多局限性,这也是为什么还有其他中间解决方案。一个好的关卡设计师应该能够放置正确类型和数量的灯光,以增强场景的外观,而不会过载 CPU 和 GPU,从而导致帧率下降。

图片由 Divine Unity Shrine,Shadowood Productions 提供,Unity 中灯光使用的示例
我们不会进一步深入细节,但您可以在以下链接中了解更多信息:
作业
想想游戏中你可能喜欢的某个特定部分。它可以在室内或室外,但必须是一个单一的位置。现在,尝试在其他游戏中找到类似的位置。例如,让我们选择一个实验室设施;比较《传送门》中的一个与《半条命》中的一个。继续这样做,直到你找到了至少五个其他例子,然后注意每个场景中的照明。它们是相似的还是不同的?它们传达的是相同的意义还是不同的意义?对于你之前没有玩过的游戏,观看在线游戏通过以获得对环境的感知。
环境
你的游戏是否设定在地球深处的一个隐蔽地牢,一个遥远的城堡,一个魔幻森林,或者可能是在一个拥有山脉和壮丽景观的异国乌托邦世界中?你如何传达你的环境可以增加你的氛围。例如,创建一个沙漠不一定是一个大片的沙地。那里有植被(即使很少),动物,在某些情况下还有残骸。然而,火星上的沙漠将非常不同于埃及或澳大利亚的沙漠。同样的情况也适用于山脉、瀑布、丘陵、冰冻土地、森林和地球表面之下。

图片来自 Unreal Engine 的开放世界演示集合:一个详细的森林示例

图片来自《Alto's Adventure》(www.altosadventure.com):一个简约的环境,但它提供了场景周围氛围的强烈感受
家庭作业
与我们处理照明的方式类似,我们也可以对环境进行操作。例如,看看许多不同类型的森林。它们是茂密还是稀疏?是否有路径引导玩家,或者它们是否被野生动物占据,或者它们是空的?注意它们的结构,并考虑为什么它们是这样的。这会给你一些设计自己环境的想法,什么有效,什么无效,甚至可能给你一些尝试相同环境不同方法的灵感。
特效
一个咒语如果没有一些魔法效果支撑,那就没什么意义,就像魔术师在挥动手或魔杖时如果没有发生任何事情,看起来会有些疯狂。特效给动作增添了特殊的东西。它还向玩家提供了一些反馈,当他们做某事时。例如,当他们发射一个致命的咒语时,你期望至少会发生一些相当壮观的事情。即使是微妙的特效,比如当你治愈自己时,也可以在创造更沉浸式的体验中起到很大的作用。
粒子系统
Unity 提供了一个强大的内置粒子系统,可以用于在场景中创建氛围。以下是你可以在游戏中实现的不同类型的粒子效果列表:
-
爆竹
-
雨
-
雪
-
萤火虫
-
余烬
-
尘土
-
泡沫
-
烟雾
-
叶子
-
爆竹
当然,这只是一些建议,实际上,任何东西都可以成为粒子效果。谁知道呢,也许在合适的环境下,Pandas 也可以变得那样。
请记住,粒子系统通常不是计算成本低的添加到你的游戏中的元素,但它们确实是一种增强你游戏视觉方面的方法。

图片来自 Unity 5.5 的烟花展示视频:使用粒子系统在 Unity 中制作的烟花
注意
值得注意的是,从 Unity 5.5 开始,实现了一个新的光照模块。因此,粒子系统中的粒子现在可以投射光线,有机会创造出惊人的效果。因此,我建议你观看以下三个来自 Unity 5.5 测试版展示的视频,这些视频展示了粒子系统的新功能:
烟花:www.youtube.com/watch?v=xAzmNo2fxWA
灰烬:www.youtube.com/watch?v=copE2b_XfTc
烟雾:www.youtube.com/watch?v=rQpgaP-r_lc
你可以在这里找到有关粒子系统的官方文档:docs.unity3d.com/Manual/ParticleSystems.html。
而在这里,有一个入门教程可以帮助你开始(尽管它是针对 Unity 的旧版本,但其中许多概念也适用于 Unity 的新版本):unity3d.com/learn/tutorials/topics/graphics/particle-system。
后处理
后处理效果可以在相对较低的计算成本下极大地增强你游戏的外观。以下是一个从 Unity 官方文档中摘取的例子;它展示了没有后处理和有后处理的场景之间的差异:


图片来自 Unity 的官方文档
你甚至可以编写自己的后处理效果。你可以从这里开始:
docs.unity3d.com/Manual/WritingImageEffects.html
因此,你绝对应该考虑在你的游戏中添加一些后处理效果,因为它们真的可以帮助你实现你心中的外观。无论如何,Unity 中后处理的完整文档可以在以下位置找到:
docs.unity3d.com/Manual/comp-ImageEffects.html
其他视觉效果
Unity 提供了其他可以在 3D 世界中放置的渲染效果。这包括绘制线条或轨迹的线条和轨迹渲染。想象一下在游戏中最浪漫的场景中出现的流星。它需要有一条轨迹,否则它只是在天空中的点移动。另一个例子可能是在实时战略游戏中,角色留下一条小轨迹,这虽然不真实,但可以增加游戏的视觉效果。
注意
值得指出的是,这些视觉效果在 Unity 5.5 中得到了很大改善,所以如果你在使用旧版本,你可能会发现它们有很大的不同。

由 Unity 官方文档提供的图片:Unity 中 Trail Render 组件的实际应用示例
小贴士
其中一些效果从计算角度来看非常昂贵。通过了解如何在图形库(GL)中编程,可以在 Unity 中重新实现这些效果的简化版本(即没有它们的所有功能),这样它们在 Unity 中高度优化,以减少性能开销。
其他效果包括光环、镜头闪光和投影仪。光环可以用来突出场景中的对象,或者只是模拟一些非常细小的灰尘。镜头闪光可以模拟太阳对相机的影响。投影仪可以用来创建相对低成本的阴影:

由《战地 3》提供图片(DICE,2011):视频游戏中使用的闪光镜头示例
音频
音频可以感动我们,让我们流泪,激励我们,让我们兴奋,有时甚至吓到我们。在某些情况下,它可以帮助我们将自己置于一个环境中。音乐的力量是真正可以增添美好特殊触感的,同时它也可以非常令人烦恼。节奏、强度,甚至乐器都可以创造不同的氛围。
音乐
音乐可以服务于特定的目的,例如用作背景音乐。它可以提供一种氛围和戏剧性,这是仅凭视觉所不能提供的。根据游戏的不同部分,音乐很可能会改变。我们可以想到现实生活中的一个例子,我们用来冥想的音乐可能与我们健身时使用的音乐大不相同。
在这个意义上,背景音乐往往会保持一致,并贯穿整个关卡。而游戏玩法通常是动态的,音乐也可以反映这一点。例如,一段由弦乐器轻柔演奏的优美背景音乐;一旦出现龙,情绪很可能会改变,音乐也是如此。以下是一些更多的例子:
-
开场/闭幕字幕、序列和剪辑。
-
一个角色遭遇战斗。
-
被追逐。
-
当一个角色进入危险区域时。一个例子是在《刺客信条》中,当玩家进入一个受限区域时。
-
当使用车辆时。
-
在对话序列中。
-
环境声音:
-
交通
-
说话声
-
机械声
-
枪声/炮声
-
水(瀑布、河流、溪流等)
-
风声
-
房间噪音(空调、电脑嗡嗡声、打字声、开关门、橱柜、抽屉、电话铃声等)
-
动物/昆虫(蟋蟀、青蛙、猫头鹰、鸟类等)
-
音效
声音触及我们的五种感官之一,它可以是一个游戏体验的重要组成部分。声音的使用可以从设定氛围、指示反馈、认可动作、为各种物品(如武器)提供音效,到指示事件发生。
使用声音不仅限于背景音乐。当我们做事情时,无论是做什么,我们都会发出声音,游戏玩法也不例外。当一个角色撞倒某物时,它应该发出声音,当他们行走、开门、拔出武器时,他们都应该发出某种声音。你可能在游戏中使用的音效包括:
-
施法
-
Ricochet
-
子弹呼啸而过
-
水花四溅
-
切割时电缆/长袍断裂
-
门吱嘎作响
-
玻璃破碎声
-
脚步声
-
从滑索上飞驰而下
引发情感
在某些情况下,你可能希望从玩家那里引发某种情感,例如当角色死亡或玩家取得胜利时。就像电影一样,当我们想要通过游戏中的某个特定方面增加情感时,尽管是在剪辑场景或游戏玩法中,音乐可以做到这一点。想想游戏中的某些时刻,你可能会在负面和正面两方面被感动。以下是一些你可能想要引发玩家情感的瞬间:
-
新角色介绍
-
角色死亡/出生
-
悲伤/快乐
-
生气/快乐
-
胜利/失败
-
戏剧性
-
令人毛骨悚然
-
浪漫/嫉妒
-
紧张
这些只是游戏中可能唤起的各种情感中的一部分。
家庭作业
在开始决定将哪些声音添加到自己的游戏中之前,进行一项很好的练习是玩一系列不同的游戏,并记录下它们使用的声音。这样做对于背景音乐和音效也同样适用。你可能会对每个游戏所包含的不同声音数量感到惊讶。还要注意音乐在不同关卡之间的过渡,音效也会随之改变吗?
请记住,你不必只玩游戏;观看电影甚至电视剧也可以很有用,看看场景/电影中何时以及是否添加了任何元素(惊吓、代表快乐)。
Unity 中的音频
音频是 Unity 中的另一个广泛的话题,从 Unity 5.x 版本开始有了很大的改进。不幸的是,我们没有足够的时间专门写一整章关于如何在 Unity 中处理音频。然而,基础概念是存在一个作为监听器的组件。每个场景中应该只有一个(活跃)监听器,否则 Unity 会给你一个警告,并且保持这种方式是一种良好的实践。然后,还有一个作为音频源的组件,它可以产生声音或播放一些音乐。此外,音频源可以被设置为 3D 模式,这意味着它们可以被放置在环境中,以便产生空间声音(玩家离音频源越*,音频源的音量就越高)。
在官方网站上,有一系列关于如何在 Unity 中使用音频的视频教程,可以在以下链接找到:unity3d.com/learn/tutorials/topics/audio
外部音频系统
如果你志向高远,而 Unity 内置的音频系统不足以满足你的游戏需求,你可以将一些第三方软件集成到 Unity 中。
最常用的一个叫做 FMOD (www.fmod.org),它是一个将空间声音集成到你的游戏中的完整解决方案。
另一个类似的工具,由于其高昂的价格而较少使用,但提供了许多功能,是 Wwise (www.audiokinetic.com/products/wwise)。你可能在你玩的一些游戏中见过它的标志。
FMOD 和 Wwise 都是非常强大的工具,但为了欣赏它们的功能,你应该看到它们在实际中的应用。网上有很多视频,我鼓励你查看它们。
团队合作
他们说养育孩子需要整个村庄,嗯,制作游戏可能需要团队,一个合作良好的团队能够完成令人难以置信的事情。然而,选择合适的人选、处理冲突以及日常合作并不总是那么简单。接下来,我们将探讨一些关于游戏开发团队中的角色、需要考虑的事项以及一些其他能帮助你走上正轨的事情。
这不仅仅关乎你,而是关于团队合作
一个好的团队就像任何良好的关系一样;你需要有沟通、考虑和理解,最重要的是你必须承认他人。团队中没有“我”,但很多时候人们会陷入“自我”。在这种情况下,忽视团队中的他人可能有多种原因,从自我、技能(或缺乏)、不耐烦、经济利益等等。当然,这并不是说这样的特质是负面的,但数量不当(就像任何事物一样),它们是导致灾难的确定方式。
当涉及到团队时,另一个问题是你们彼此认识的时间长短。这既不是坏事,也未必总是好事。特别是如果你打算与对方做生意,重要的是要保持专业。你需要交付产品,需要满足截止日期,可能还有参与你项目的利益相关者。你必须能够将商业和友谊分开。动态会改变,你需要避免让它变得个人化。
游戏开发团队将包括不同的角色,你的团队规模决定了这些角色的执行方式。在理想的世界里,你将为每个角色配备团队成员,但在大多数情况下,你将不得不承担双重责任。在此说明,一个典型的团队可能包括以下成员:
-
项目经理:为团队设定里程碑并确保它们得以实现。但这不仅仅是时间管理;项目经理还应鼓励和激励落后于截止日期的团队和成员。他们帮助提升团队士气,并确保项目顺利且更重要的是按时进行。
-
制作人:将是处理事务商业方面的人。他们负责维护预算、时间表和营销策略。他们在项目管理员那里的不同之处在于,他们更关注物流和技术细节,而不是管理团队的运作。
-
社交媒体策略师:指的是那些致力于你游戏所有社交媒体和营销方面的人。通常,制作人也会负责这一部分,但在某些情况下,根据你希望利用的社交媒体程度,有专人负责推广你的游戏可以帮助释放预算和时间,以及你团队制作人的财务方面。
-
设计师:将是将所有元素结合起来以创造整体体验的人。他们确保所创造的内容与故事相辅相成;他们决定如何向玩家展示,以及玩家最终如何与游戏流程互动。
-
艺术家:负责创造游戏的外观,从角色的概念艺术、资产和关卡,到它们的模型和纹理。如果你能的话,拥有不止一个艺术家是理想的,尤其是如果你的游戏规模较大。艺术需要时间,尤其是如果你想让你的游戏在美学上具有魔法般的效果。
-
程序员:就像项目的手臂一样;没有他们,你很可能难以让你的精彩想法变为现实。说到这里,如果你不幸没有程序员在你的团队中(或者人数不足以开发你可能想要的全部功能),许多游戏引擎提供了可视化脚本,允许你将命令链接起来以创建动作。一个很好的例子是 Unreal Engine 中的 Blueprints,如这里所示:
![这不仅关乎你,还关乎团队合作]()
图片由 Unreal Engine 提供,蓝图可视化脚本(
docs.unrealengine.com/latest/INT/Engine/Blueprints) -
声音设计师/作曲家:让游戏听起来很棒的人。声音设计师确保每个动作都有某种形式的音频反馈。游戏中的音频包括音效和背景音乐。声音设计师通过让游戏听起来生动来贡献氛围。即使是微小的效果,例如当玩家靠*某个黑暗和危险房间时,也能唤起各种各样的情绪。
-
质量保证(QA)测试员:负责确保游戏正常运行的人(或人们)。这可能包括朋友、家人、专业游戏玩家和质量保证测试员。理想情况下,你希望 QA 测试员不参与你项目的创建,因为尽管你自己也会进行测试,但很容易忽略可能影响整体游戏体验的游戏方面。
-
作者:通常负责开发游戏故事的人。故事元素可以包括整体情节、角色、背景以及其他叙事元素,如对话和叙事路径,例如不同的结局。本质上,作者负责确保故事不仅能够吸引和吸引玩家,让他们渴望更多,还要营造氛围并增强整体游戏体验。此外,作者还必须确保故事合理、逻辑清晰且具有解释性。最后,需要记住的一点是,为游戏写作需要与电影或阅读文本不同的方法,因为它是互动的。
分享一个共同的愿景
当你在团队中工作时,拥有一个共同的目标和共同的兴趣非常重要。虽然这不用说,但在团队项目中,士气是关键。项目会有困难时刻,会有熬夜和压力水*上升的时刻,但得到他人的支持可以改善大家应对所有困难的方式。会有你忙碌、无法完成或工作过度负荷,以至于阻碍你完成任务的时候。当然,如果你很懒惰,那完全是另一种情况,但你应该始终与团队成员沟通任何困难,并且尽可能快地沟通。为了真正说明这一点,考虑一下蝴蝶效应的诗意概念:巴西一只蝴蝶的翅膀拍动会在德克萨斯州引发龙卷风吗? 开始时的小(未解决)问题最终会变成更大的问题。
管理期望
拥有一个共同的愿景并不总是等同于拥有相同的期望。例如,你和你的团队可能想要制作一个高销量且受欢迎的应用程序,但每个团队成员愿意投入的时间和精力可能会有所不同。因此,为了许多原因,在早期就建立期望也很重要。
除了期望之外,确保花时间正确规划你的游戏开发。如果你时间很少或者你的游戏包含必要的组件,你不希望过度规划你打算做的事情。同样,这也适用于规划不足的情况,你决定了游戏的设计,但没有给自己足够的时间来完成它。最终,你的团队可能会开始削减部分内容,以便达到目标。最终,你可能会得到一个类似于游戏演示版本的东西,而没有令人兴奋的完整版本来保持玩家对你的作品的兴趣,或者更糟糕的是,你甚至可能没有完成的游戏。
最后,永远不要假设任何事情。假设很可能会在以后导致灾难。例如,你看到那些精心规划的甘特图,上面列出了要完成的事情,在这个阶段,你的艺术家们应该正在建模资产,但现实中他们并没有;或者至少有一个艺术家没有,因为他们认为其他艺术家会做。定期开会,有一位优秀的项目经理和制作人可能会避免这种情况发生,但始终要了解其他人正在做什么。
协作和沟通是关键
在一个群体中最糟糕的事情就是停止交谈。也许事情陷入了低谷,人们变得忙碌,也许某个成员的个人生活中发生了某些事情,无论如何,总会有一些事情会吸引我们的注意力到别处;毕竟能力有限。然而,这并不是团队停止交谈的理由。如果沟通中断,找出原因并在早期采取行动。也许问题很小,可以轻松解决。在其他情况下,它可能会损害项目的成功,例如,由于团队成员太尴尬而不承认他们无法完成任务,或者只是太忙,但直到最后一刻才通知任何人。避免此类问题的一种方法是在群体内使沟通变得容易,并且经常沟通****。
沟通方式
虽然这很明显,但以下是一些相对简单的方法,通过以下方式保持联系:
-
电子邮件:电子蜗牛邮件非常适合保持对话的线索,但它可能变得相当难以管理。在这些情况下,定期(例如,每周或每月)进度更新会非常理想。给它一个截止日期和模板。例如,一个他们需要涵盖的事项清单,如目前正在进行的任务、问题和未来的工作。这样,你就能跟上每个人的进度,其他人也是如此。为了使整个过程更容易管理,大多数(如果不是所有)电子邮件软件都有诸如标记和过滤等功能。在这些情况下,与指定标准相关的电子邮件,如电子邮件地址或主题,将被定向到特定的文件夹,并/或添加标签,以便以后更容易检索。
-
WhatsApp:你很可能有其他团队成员的电话号码,无论是个人还是商务号码。因此,像 WhatsApp 这样的电话信息服务是保持联系和进行通话的绝佳方式,无论他们身在何处。
-
社交媒体群组和聊天,如 Facebook。使用 Facebook,你可以通过多种方式保持沟通渠道畅通。你可以创建一个消息组或一个私人组。两者之间的区别很小,因为它们的功能几乎相同,你可以上传文档并进行讨论。然而,一个群组的功能与 Facebook 页面非常相似。所以如果你上传一张图片,它将创建一个单独的帖子,而不是在聊天中,它只是作为对话中下一部分的对话添加。创建 Facebook 群组的优点是,在查找某些项目方面更容易管理,并且可以使对话更具体地针对帖子(或主题)。
-
Skype/Google Hangouts 等工具:节省打字时间,并通过电话与人沟通。永远不要低估语音/视频通话的力量。虽然有时,尤其是在远程工作时,保持超越文本通信的联系很重要。它为工作关系增添了更多个人化的层面,并且您能更好地了解对方,而不是试图从字里行间解读。
版本控制
对于艺术家来说,通过使用云存储服务(如我们稍后将要讨论的 Google Drive 或 Dropbox)共享文件,如纹理和网格,是一种有效的解决方案。然而,当涉及到程序员和开发者时,共享文件的情况就不同了。在游戏开发过程中,程序员倾向于与他们的代码和脚本文件进行协作。因此,有一些重要的需求应该被考虑:
-
程序员在相同的源代码上共同工作。这可能涉及(但不限于)在项目内部的不同区域以及相同的文件(们)上进行协作。因此,如果没有某种形式的文件控制,可能会变得非常混乱,无法跟踪哪些内容已被修改。
-
在项目中进行编程的一般性质将涉及利用一系列不同的技术,并尝试各种解决方案来解决一系列问题,以最适合项目。因此,如果进行了大的更改或不是必然正确的解决方案的更改,程序员需要有一种方法可以回到之前的版本。
-
从其他来源,如库和源文件中重用全部或部分代码,在相关的情况下可以节省大量时间。通过这样做,可以为具有类似元素的项目部分节省时间,从而为更复杂的部分留下更多时间。
通过实施某种形式的版本控制,程序员能够利用一个文件系统,它不仅能够识别和跟踪文件更改,还允许程序员撤销这些更改。此外,版本控制还可以帮助程序员更方便地重用和整合来自外部来源和/或项目的代码。这些功能以及更多,都由版本控制软件支持。
Git 是一个免费且开源的版本控制程序。您可以通过访问 Git 主页在此下载它:www.git-scm.com。一旦安装并正确配置,它就可以用于对您的文件应用版本控制,从而成为您工作流程中的一个不可思议的资产。
小贴士
有一些可视化工具可以更轻松地管理 Git。其中最完整的一个是 Atlassian 的 SourceTree (www.sourcetreeapp.com)。然而,如果您是新手,可能发现使用 Axosoft 的 GitKraken (www.gitkraken.com) 更容易。
此外,如果你想学习 Git,这里有一个快速且有趣的交互式教程:try.github.io。
制定 GDD 并坚持执行
在开始时,每个人都会有想法、概念或想要添加到游戏整体开发中的东西。在这个阶段,这是非常好的,因为它将给你很多思考的空间,当涉及到精炼你的游戏时,它将为你提供一系列不同的选择。
游戏设计文档(GDD)是一种整洁的方式来拥有你游戏当前的状态和将来的状态。将 GDD 视为一本手册,定义了你的美学、音频、命名约定、故事、角色以及其他游戏碎片。它是一个参考点,如果你愿意,可以称之为游戏圣经。GDD 的结构各不相同,没有正确或错误的方式去做,因为它将完全取决于你的游戏。然而,有一些部分定义了游戏的内容。例如:
-
简介:
-
你的游戏是关于什么的?想象一下这是一个电梯演讲。保持简短、尖锐、直接。你不想涉及太多细节,但足以传达你游戏的整体大意。例如,Sugar mountain, Panda invasion 是一款 2D 即时塔防游戏,玩家必须用各种可食用的投射物击败熊猫。
-
你的游戏针对的是谁?儿童、成人,或许两者都是?理想情况下,这是你考虑你想要吸引到你的游戏中的受众群体的地方。
-
你的游戏将在哪里展示/在哪些设备上?Android、iOS、Windows、Mac、Linux 都带来了不同的挑战,需要不同的考虑,所以请记住这一点,并在早期定义,以减少后续的头痛。
-
-
艺术:
-
定义你想要达到的美学风格的氛围板。这些可以是通用的,也可以是针对角色或级别的特定内容。它们有助于在创建概念艺术时指导艺术方向。
-
概念艺术包括定义游戏当前状态的一切。当然,之前的艺术作品可以保留在 GDD 中,但最好将其移至附录或单独的部分,以保持文档的相关性。
-
UI/GUI/HUD 都与交互元素的外观有关,以及基本上不是游戏中的其他事物将如何呈现。例如,如果你有一个 HUD(抬头显示),它将在游戏中如何呈现。它将占据屏幕多少空间,以及它将如何与当前的美学风格协同工作?
-
需要绘制角色插图,以提供关于他们外观的线索,然后进行建模(或绘制 2D 图像)和动画。绘制角色在不同姿势、环境和情境下的样子,有助于更详细地展示角色的个性和整体外观。
-
将填充你的游戏环境的资产,这些资产将在稍后创建(绘制或建模)。
-
-
音频:
-
为游戏营造氛围的背景音乐。它可以很简单,比如提供探索环境的氛围音乐,或者当玩家接*游戏中的某个重要部分时作为音频提示。
-
声音效果不仅提供反馈源,例如当物体与表面接触时,还包括当发生事件时,如玩家获胜或失败。
-
-
故事:
-
角色传记将包括与每个角色相关的信息,从他们的基本信息(年龄和外观)、背景故事、性格以及他们在游戏中的与其他角色的关系。根据你的游戏类型,它将是概括性的或极其详细的(如角色扮演游戏)。
-
您游戏的大致情节将为每个人提供游戏的主要内容。就像角色传记一样,细节程度将取决于游戏类型。例如,如果是一款像数独这样的游戏,你可能甚至没有故事(除非当然你的解决方案解锁了通往解救公主的门)。
-
叙事流程将解释随着游戏的进行,叙事是如何被揭示的。把它想象成一个地图,指示何时以及如何将叙事的各个部分揭示给玩家,以及游戏的每个部分如何与叙事相关。例如,让玩家穿越地下洞穴并不能救出被困在地图另一侧城堡中的公主,除非当然洞穴有一个秘密通道可以带你去那里。就像游戏玩法一样,你的叙事需要与你在游戏中的行为相关联,反之亦然。
-
-
技术:
-
管道概述将简要解释将要用于构建游戏的软件(游戏引擎、第三方插件等)以及如何将它们连接在一起。此外,它建立了艺术和软件之间的联系,以及它们如何进行通信(例如,图形应该如何打包以便在软件/游戏中使用)。
-
技术设计将解释您游戏的不同部分是如何连接起来的,并建立规范。此外,它包含接口应该如何实现,以便在项目内部保持一致性,以及如何将工作封装在任务中(可以由不同的人完成)以及如何将它们集成在一起。通常,技术设计是在技术设计文档中,与游戏设计分开,因为它只由程序员使用。
-
系统限制将解释所使用技术的局限性,向非程序员团队成员说明(实际上,更详细的限制在技术设计文档中)。就像管道概述一样,这份文档为程序员、设计师和艺术家之间提供了一个桥梁。
-
-
游戏流程:简要说明游戏将如何进行。从一个关卡/剪辑场景/等等,到下一个。与叙事流程类似,游戏流程讨论了随着玩家在叙事中前进,他们将在游戏中做什么。例如,他们将会去地图的 A 部分,杀死 20 条龙,然后发现神奇的药水,接着是剪辑场景揭示他父亲的真相,然后玩家被传送到一个魔法岛屿去探索家族档案。通过这样做,你开始看到你的游戏和/或故事中的漏洞,并能够确保它不仅合理,而且进度以玩家会喜欢的速度进行;也就是说,游戏既不会太快也不会无聊和令人困惑。
-
时间线:它指的是你的游戏从开始到结束所需的时间,以及所有进度里程碑和截止日期,以确保你的游戏能够达到发布日期:
-
里程碑就像是推动你的项目进一步发展的垫脚石。这些通常是游戏达到一个重要节点的时候,比如艺术资源完成、酷炫功能实现、第一个工作原型等等。里程碑可以像你想要的那么频繁或不频繁,但它们是跟踪每个人进展的一种方式,以确保工作在时间框架内完成,以达到不同的项目目标。
-
截止日期可能听起来令人畏惧,因为如果这是一个重要的截止日期,它可能会毁掉一个项目,并最终造成(尽管是金钱和时间)的损失。
-
重要日期非常重要。它们是每个人都需要到场、现场(如果你有一个实体位置)、在线(如果你有一个虚拟位置),或以任何其他形式出现的日子。这些日期可能出于各种不同的原因,包括利益相关者、客户,甚至潜在雇主会议。团队成员应该认真对待这些会议,准时到达,并做好准备。还应该记录会议笔记,以便记录发生的事情。
-
使用项目管理工具保持整洁
没有人喜欢杂乱的工作空间;即使是有序的混乱也有其极限,当你需要那份文件时。当你开发可能有很多文件的游戏时也是如此。因此,我们必须找到方法来不仅管理文件,还要管理团队成员,以便我们所有人都知道文件在哪里,并且有与他人沟通关于它们的方式。市面上有许多团队和文件管理程序,其中一些最受欢迎和有用的将在下面讨论。
Slack
Slack (www.slack.com) 是最有效的团队管理工具之一。它将你所有对话、主题、待办事项列表、笔记以及项目中的许多其他碎片都集中在一个地方。
其中一些功能在这里进行了说明:
-
频道将主题分开,使讨论更加集中。把它们想象成有特定讨论主题的房间。例如,你可能有一个专注于你项目艺术的美术频道。它还提供了一个地方,让其他团队成员可以请求或检查某些项目的进度。频道可以像你希望的那样广泛或狭窄。
-
该应用使得在移动中管理 Slack 变得更加容易。它允许你在组成员上传和/或发布任何内容时接收实时更新。此外,它还允许你随时随地与 Slack 进行互动。例如,你得到一个很好的想法,或者你可能看到对你正在开发的概念有用的东西,使用 Slack 应用,你只需要发布或捕捉你的想法并将其提交到频道。你可以把频道想象成群组。
-
Slack 的应用目录允许你将其他应用程序(其中一些我们将在稍后探讨)集成到 Slack 中,使其成为终极项目管理软件。你可以在
slack.com/apps找到应用目录。

图片由 Slack 教程视频提供 (slack.com/is)
HacknPlan
类似于 Slack,HacknPlan (www.hacknplan.com) 是另一款团队管理软件。然而,它更专注于游戏开发。例如,你可以提供工作的时间表,为每个任务分配点数或价值,这些点数或价值可以用来确定游戏特定部分的支付和/或创作者的贡献。它允许你一眼就能看到项目当前状态的相对详细概览。如果你查看下面的截图,你可以看到有多个不同的看板,每个看板都指示着生产流程的不同部分,从尚未开始的计划到已经完成的完成。我推荐你尝试一下,因为这是我最喜欢的工具之一!

图片由 Press Kit 提供 (hacknplan.com/press/#!)
Drive
Google Drive (drive.google.com),以及其整个程序套件,作为一个公共资源非常灵活且有用。它不仅提供了类似于其他知名商业产品的程序,而且还具有使整个过程更加容易的共享功能。你可以轻松地创建和共享文档、文件和文件夹,无论是与许多人还是少数人共享,都可以通过私有链接或通过权限进行。你可以以多种方式控制对文件的访问权限(从编辑到只读);这都将取决于最初分享它们的目的。

图片由 Google Drive 提供
小贴士
在文件和文档创建方面,Drive 的一些替代品是 Apache OpenOffice (www.openoffice.org) 和 ONLYOFFICE (www.onlyoffice.com)。
Dropbox
与 Google Drive 类似,但增加了其他软件,Dropbox (www.dropbox.com)提供了一个用于管理项目文件的灵活工具。它允许你上传文件、创建文件夹,并共享它们以及其中的文件。因此,在文件共享方面,它是 Drive 的一个很好的替代品。此外,对于 Word 文件等文档,它允许你与其他团队成员实时编辑它们,同时显示正在进行的或已完成的编辑。

图片由 Dropbox 新闻稿 (tinyurl.com/DropboxPressKitScreenshot) 提供
Trello
Trello (www.trello.com)某种程度上类似于任务管理的虚拟图钉板。它允许你创建一个看板,其中保存了所有任务。每个看板都有不同的面板,展示任务。当然,这些面板的顺序可以根据你打算用 Trello 做什么样的流程来调整。例如,创建一个管道风格的 Trello 看板可能包括艺术、设计、编程和音频等不同区域;所有这些区域都分配了特定于其领域的任务。例如,艺术部分可能包括主菜单的概念或公主塔的概念艺术等任务。每个任务也可以分配给特定的人,或者留给小组中的任何人参与。一旦任务完成,它可以被移动到另一个区域,当然,这个区域被恰当地命名为“完成”或简单地留在列表底部。你如何使用 Trello 完全取决于你,但它确实有助于使流程更加流畅。

图片由 Trello 之旅 (trello.com/tour) 提供
Redbooth
另一个选项,某种程度上是 Trello 和 Dropbox 的结合,是 Redbooth (redbooth.com),也被称为 Teambox。Redbooth 允许用户分享文件,分配任务给团队成员,并发送直接消息。像我们在这里查看的许多其他应用程序一样,它也会在项目状态更新时提醒你。

图片由 Redbooth (redbooth.com/?ref=teambox#!/dash) 提供
GitHub
之前我们讨论了版本控制。然而,你仍然需要一个地方来存放你的仓库。GitHub (www.github.com) 为 Git 仓库提供托管服务。对于公共仓库是免费的,这意味着每个人都可以看到你上传的内容(这个选择是为了鼓励开源软件)。否则,它为你的仓库提供付费托管计划。

图片由 GitHub 提供
BitBucket
Bitbucket (bitbucket.org) 是一个与 GitHub 非常相似的服务,用于托管你的 Git 仓库。在这里,你可以免费拥有私有仓库,但限制在于用户数量。最多五个用户是免费的(这对于小型团队来说非常完美);否则,你需要切换到付费计划。

图片由 Bitbucket 提供
日历
另一个直观的软件(甚至硬件)是日历,但更重要的是一个供你团队共享的日历。设定截止日期很重要,但要确保每个人都遵守,请使用公共日历;当即将到来的事件、会议和里程碑临*时,它会向每个人发送通知。

图片由 Google 日历 (play.google.com/store/apps/details?id=com.google.android.calendar&hl=en)
传达你想法(们)美学部分的一个好方法是情绪板。Pinterest (www.pinterest.com) 是这个方面的绝对神奇工具。你不仅可以创建不同的板来固定图片,而且它们还可以在团队成员之间共享,并设置为私有。

图片由 Pinterest 提供
Hootsuite
在一个地方管理多个社交媒体渠道的一个简单方法可能是 Hootsuite (www.hootsuite.com)。它允许你安排在各个*台(例如 Facebook、Instagram 和 Twitter,我们稍后会看到)上的帖子,这样你就可以使用应用程序处理其他重要的事情。
此外,它还允许你一目了然地查看每条帖子是如何被互动的,例如点赞、推文和收藏。

图片由 Hootsuite 提供 (signupnow.hootsuite.com)
精炼你的游戏
好吧,我们已经提交并抛光我们的游戏,使其成为杰作,现在是时候给它添加一些光泽了。为了使我们的宝石发光,我们需要用一些事情来使其工作得更好,同时确保它将按我们的意愿工作。到现在为止,你应该已经测试过它(不止一次),因此你会有一些关于需要改进之处的想法,例如卡顿。在接下来的章节中,我们将探讨优化游戏各个部分的方法,以便在你发布时,你将拥有一个运行流畅且有效的游戏。
处理力量
在你真正深入游戏开发之前,有一件事你需要确保做的是确定它是否针对你打算投放的设备进行了优化。通常优化被视为游戏开发的最后一步,但这并不正确。你越早考虑开发流程中的优化,后期优化游戏所需的努力就越少。
笔记
Unity 为优化移动*台提供了丰富的资源,例如特定的优化,如脚本和图形。你可以在以下链接找到它们:docs.unity3d.com/Manual/MobileOptimizationPracticalGuide.html。
不同的设备都有自己的问题需要克服。就像我们的大脑一样,移动设备(以及一般的计算机)可以一次处理大量的信息。因此,这最终会影响你游戏的整体性能。例如,图形(2D 和 3D)或代码效率低下等问题可能会非常消耗资源。最终,这会影响游戏的运行,导致卡顿。除了设备之外,根据你设计的维度(2D 或 3D),还需要考虑不同的因素,我们将在下一节中探讨。
检查构建大小
你需要考虑的许多事情之一,是游戏的大小,尤其是因为它将影响其分发和/或发布方式。因此,检查你的构建大小对于理解游戏的大小至关重要。你可以直接构建游戏(如何操作,将在本章后面解释),并检查其大小。然而,这并不能给我们太多关于我们可以减少什么的信息。
在你构建完游戏后,你可以右键点击控制台窗口,然后选择打开编辑器日志,如图所示:

你的操作系统将使用默认的文本编辑器打开Editor.txt文件;在末尾附*,你可以找到一些关于你上次构建的统计数据,特别是你游戏不同元素的大小。你应该会有类似以下的内容:

与单个文件相关的细节(只显示一部分)如下:

如你所见,我们游戏的大部分大小都是由纹理占用的,这在 2D 游戏中很常见。让我们在下一节中探讨如何减小纹理的大小。
纹理优化
一些图像格式可能会出现问题,例如文件大小。幸运的是,在 Unity 中,你可以通过多种方式减小文件大小:
-
改变图像分辨率。
-
改变 Unity 处理该特定图像的方式。
在 Unity 中,你可以在导入设置中更改这些设置。要这样做,只需从你的项目文件夹中选择一个资产,然后会出现以下屏幕:

在前面的章节中,我们解释了一些这些设置,主要是与游戏相关的设置。但正如你从前面的截图中所看到的,还有许多其他选项可以设置纹理,使其在游戏中达到最佳状态。
小贴士
如您从之前的截图中所见,有一个关于 POT 纹理的警告。那是什么?2 的幂(POT)纹理是边长为 2 的幂的方形图像(例如 2、4、8、16、32、64、128、256、512、1024、2048、4092 等等)。由于我们拥有的硬件运行在二进制系统上,2 的幂非常重要,因为某些技术允许以某种方式处理它。因此,性能可以得到提升。在特定情况下,警告指出需要压缩成 DXT5 格式。因此,尽管很难让所有纹理都成为 POT 纹理,但您应该考虑尽可能多地使用它们。
如您从之前的截图中所读到的,我们的熊猫精灵图集是一个NPOT(非 2 的幂)纹理,因为这张精灵图集是为了清晰度而设计的,用于在第四章不再孤单——甜味熊猫出击中学习动画,而不是为了优化。
为了提高游戏性能,最重要的参数是:生成 Mip Maps、过滤模式、最大尺寸、压缩和格式。为了便于理解,它们已在之前的截图中被突出显示:
-
生成 Mip Maps: 这是一个选项,如果选中,将创建纹理/精灵的小版本。因此,当精灵相对于相机较远或较小时,将渲染较小的版本。因此,在运行时可以获得性能提升,但可能会增加构建的大小。
-
过滤模式: 允许您对图像应用过滤器,特别是使边缘略微模糊。当处理像素艺术时,一些过滤器可能很有用。这是因为过滤器使您的资产不那么像素化。过滤模式选择当纹理被变换拉伸时如何过滤纹理:
-
点: 纹理在*距离时变得块状
-
双线性插值: 纹理在*距离时变得模糊
-
三线性插值: 这类似于双线性插值,但纹理也会在不同的 Mip 级别之间模糊
-
-
最大尺寸: 如其名所示,对图像在该特定*台上的最大尺寸进行限制。实际上,虽然高分辨率图片或图形可以增强体验,但从性能角度来看,对于应用程序来说并不总是最佳选择。此选项允许您在牺牲质量的情况下,大幅减少构建大小,以防出现相关问题。
-
压缩和格式: 指定在游戏编译/构建时图像是否会被压缩。重要的是要记住,如果您的目标设备是特定*台或非常老旧,它可能无法支持某些压缩格式。再次强调,质量和性能之间存在权衡。
小贴士
练习:
由于我们没有时间详细查看所有单个选项,作为一个练习,找出所有其他设置在官方文档中的工作方式。然后,稍微玩一下它们,直到你感到舒适并真正理解它们如何影响开发工作流程。
统计数据和性能分析
在创建你的游戏时,优化将变得困难,尤其是如果你直到最后一刻才开始担心这个问题。主要问题之一(至少在 3D 游戏中)是多边形计数,这是你的 3D 资产拥有的面数。例如,如果你想到一个立方体,每个面都是一个多边形,因此立方体将总共拥有六个多边形(一些显卡不支持四边形,因此立方体的六个面应该分成两个三角形,总共 12 个多边形)。说到这里,需要考虑的是累积的总数。如果太低,你的游戏看起来可能会相对粗糙;如果太多,尤其是对于移动设备,很可能会出现卡顿。要检查 Unity 场景中的多边形计数,请转到游戏视图的右上角,并检查统计数据选项,如图中所示:

为了更好地了解你应该如何建模,在 Unity 的用户手册中,有一页解释了如何建模资产和角色以便进行优化。你可以在这里阅读:polycount.com/discussion/130371/polygon-count-for-smartphone-applications。
完成此操作后,应该会出现如图所示的弹出窗口。此弹出窗口中的统计数据显示了与不同事物性能相关的信息,例如绘制调用。此外,它还指示当前场景视图中有多少多边形。实际上,这个 S统计数据屏幕的一个巨大优点是,当你按下播放时,你可以实时使用它,这样你就可以观察游戏中的哪些部分比其他部分更占用资源(我们之前在第二章中提到过,烘焙纸杯蛋糕塔):

最重要的参数要检查(如图中所示)是:
-
FPS (每秒帧数): 如其名所示,这显示了你的游戏每秒可以产生多少帧。当然,这个值可能会因不同设备和/或计算机的硬件而大幅变化。你应该确保你的游戏能够在每个目标*台上至少以 50/60 FPS 运行。
注意
在某些情况下,你可能想限制最大 FPS 值,原因有两个:
一些显示器具有有限的刷新率
人眼也有有限的刷新率
因此,你可能想要避免生成比你实际需要的更多帧,从而造成计算资源的浪费。
-
Tris(三角形)和Verts(顶点):它们表示当前帧中从活动相机(可能不止一个活动相机)渲染的三角形和顶点数量。特别是在 3D 游戏中,这一点至关重要。Tris和Verts越多,你可以包含的细节就越多,但这是以性能为代价的。
-
SetPass 调用:这表示 Unity 需要经过多少次迭代才能渲染出特定的帧。你应该尽一切努力尝试减少这个数字。迭代次数越少,Unity 渲染特定帧的速度就越快,从而提高你的帧率(FPS)。
如果你需要更多关于你的游戏资源使用的高级统计信息,你可以通过从顶部菜单栏的Window | Profiler导航到Profiler来访问它。你应该有一个类似于以下屏幕的界面:

窗口被垂直拉伸,以便尽可能多地显示类别。然而,由于空白空间将被图表和你的游戏的不同部分填满,分析窗口应该尽可能放大。如果你有双显示器设置,考虑将分析窗口放在你的第二显示器上。
这个强大的工具允许你详细监控你的游戏的不同组件如何使用资源。Profiler 通过分析 GPU、CPU、内存、渲染和音频的性能来帮助你优化游戏。因此,你可以了解你的游戏在不同区域花费了多少计算能力。例如,你可能想知道渲染、动画或甚至游戏逻辑花费的时间百分比。
注意
你可以在官方文档docs.unity3d.com/Manual/Profiler.html中或从官方网站unity3d.com/learn/tutorials/topics/interface-essentials/introduction-profiler的视频教程中了解更多关于 Profiler 的信息。
此外,你还可以进行远程分析;在docs.unity3d.com/Manual/ProfilerWindow.html中,接*页面底部,你会找到一个名为远程分析的部分。它解释了如何根据你想要远程分析的*台进行操作。
其他优化技巧
另一种优化游戏的方法是限制 3D 资产上的材质数量(这是 Unity 在游戏对象上应用着色器的方式)。例如,有时你可以将许多不同的材质(和贴图)应用到对象上,以产生一系列酷炫的效果;然而,少即是多。对于在电脑或游戏机上玩的游戏,这并不是一个大问题,但需要考虑。对于移动设备上的游戏,重要的是要记住,它们能够处理一些酷炫的效果,但一切都有极限。在游戏开发的早期阶段考虑到这一点,如果需要调整游戏的一些组件,将有助于减少后期阶段的工作量和压力。有些材质在计算上可能非常昂贵。有一些着色器可以很好地*似酷炫效果,但针对移动设备进行了优化(例如我们之前讨论过的光晕效果)。
在这个小段落中探讨的想法只是从图形角度优化应用程序的冰山一角。然而,你可以在这里继续阅读:docs.unity3d.com/Manual/OptimizingGraphicsPerformance.html。
另一种优化游戏的方法是关注你的代码。
在 Unity 的早期版本(5.0 之前),需要缓存 Transform 组件。正如你可以在本文末尾的blogs.unity3d.com/2014/06/23/unity5-api-changes-automatic-script-updating中读到的那样,现在不再需要这样做。然而,仍有开发者在使用它;现在应该避免使用,因为它会大大降低代码的可读性,从而影响开发过程。无论如何,你仍然应该将其他组件缓存到变量中,就像我们在第三章中为 UI 所做的那样,与玩家通信 – 用户界面。
注意
如果你正在使用 Unity 的旧版本,或者你只是想了解 Transform 组件的缓存是什么,请查看以下代码行:
transform.position = Vector3.zero;
在过去版本的 Unity 中编译时,它相当于:
GetComponent<Transform>().position = Vector3.zero;
此外,正如我们在各个章节中已经提到的,GetComponent() 函数运行缓慢,在可能的情况下应该避免使用。因此,你应该考虑将 Transform 组件缓存到一个变量中(就像我们在第三章中做的那样,与玩家通信 – 用户界面,我们将 UI 组件的引用存储到变量中)。因此,在 Transform 的情况下,你可以在 Start() 或 Awake() 函数中执行以下代码行(当然,在声明了 thisTransform 变量类型为 Transform 之后):thisTransform = GetComponent<Transform>();
其他技巧涉及字符串,在 Unity 中到处都广泛使用。然而,除非你正在进行特定的字符串操作,否则你应该避免对字符串进行这里提出的优化,因为性能提升可能不值得花费的时间和代码可读性的降低。以下是一些技巧:
-
记住,如果你在单个调用中连接不同的字符串(这意味着,例如,在一个不在 Coroutine 中放置的 while 循环中执行,这样它就会在一个帧中全部计算),内存可能会迅速填满未使用的字符串对象,而没有给垃圾收集器清理它们的时间。因此,你可以使用
StringBuilder类,它也稍微快一些。 -
比较不同字符串的方法有很多。然而,最快的方法是以下这种方法:
firstString.Equals(secondString, StringComparison.Ordinal);
- 原因是算法只需要遍历两个字符串(这里的单个字符被视为数字,因此这种方法是区分大小写的)来查看是否有任何差异。
游戏测试
测试,测试,再测试!不仅要在你的目标设备和受众中进行测试,还要与他们一起测试,测试,再测试!这看起来可能是游戏开发过程中的一个明显部分,但在这一阶段,即使你自己和你的团队成员进行测试,这也是绝对关键的。确保游戏在高性能和低性能设备上都能运行,这些设备都在你的规格范围内。一个很好的视频是关于游戏测试的额外学分视频,可以在以下链接找到:tinyurl.com/PlaytestingExtraCredits。
不要假设如果你的游戏在你的或你的朋友的设备上运行良好,它就会在每个人的设备上运行。已经有过游戏在某些手机型号上无法运行的情况。鉴于这种情况,尽可能多地测试。然而,幸运的是,如果你无法获取一系列不同的设备,你可以使用模拟器来模拟它在特定设备(或设备)上运行。Genymotion (www.genymotion.com) 是一款提供模拟不同设备优秀方式的产品。
你为什么要进行游戏测试呢?
这是整个试玩测试中最基本的问题:你为什么要试玩你的应用程序?当然,答案很明显:为了获取反馈。但是为什么?例如,你想要看看新增的功能是否必要吗?也许你想要找出音效是否过于频繁且令人烦恼;或者也许音效还不够。
练习
召开团队会议,坐下来审查你的应用程序拥有的功能,哪些难以实现,哪些是新颖/实验性的,或者甚至哪些应该被移除。是否有过任何非正式的测试,提供了关于游戏的另一个视角?你需要重新定义项目,并且每个人心中对游戏的概念是否相同?你可能已经这样做过,甚至几次,但如果你还没有,定期审查你的游戏及其开发是一个好的实践。
你需要试玩测试谁?
你想要和需要谁试玩你的游戏?是多人游戏、单人游戏还是合作游戏?理想情况下,当你进行试玩测试时,你希望人们来自你的目标群体。在他们试玩时,你希望鼓励他们做笔记,在试玩前(他们的期望是什么?),在试玩中(他们在想/感觉什么?),以及在试玩后(可以改进什么,游戏的好/坏之处,缺少什么等等)。
单人测试运行
在邀请任何组外的人测试你的游戏之前,最好先测试你的游戏。虽然这看起来可能很明显,但即使你认为你的游戏无懈可击,也很可能有些东西没有按预期工作。这就是为什么自己和你/你的团队进行试玩测试很重要,因为如果你排除了错误和漏洞,那么玩家花在尝试玩你的游戏上的时间就会减少,花在提供有价值的反馈上的时间就会增加。说到这里,在正式的试玩测试之前,与一小群其他人进行试玩测试甚至更有价值。理想情况下,这可以在你的团队内部轻松完成。当你这样做的时候,不仅可以帮助你更好地了解如何与其他人测试应用程序,而且还能帮助你塑造他们对实际体验的疑问。
使其成为一个社交活动
人多力量大,试玩测试也是如此;但与此同时,确保它在你可用的资源范围内是可管理的。当你组织团队测试时,重要的是要混合来自你的目标受众群体之外的人。这是因为你可以获得不同视角的反馈,这可能会提供关于游戏如何或如何不起作用的见解。
在界限内进行
这等同于询问你的试玩测试是公开还是私密的。在这个阶段,你需要问自己游戏是否包含可能影响发布的敏感成分。因此,你可能需要玩家签署一份保密协议(NDA),这阻止他们公开讨论游戏、分享截图,以及因此产生的法律后果。相比之下,他们在试玩后可能可以自由地、公开地与任何人谈论游戏。
在试玩者开始注册之前,这一点非常重要。你不想在 Facebook 或 Twitter 上登录时,看到人们发布关于你还在开发中的应用程序的截图。其他考虑因素可以要求试玩者不要在测试期间拍摄任何视频或照片,包括自拍、签到和社交媒体帖子。在某些情况下,打印屏幕选项被禁用就是为了这个目的。当然,人们很聪明,他们会找到许多解决方案来解决这个问题。但如果你有某些措施和后果,确保参与者在试玩前遵守一套规则,并承认这些规则(例如,签署协议),你就可以避免之后出现任何不希望的宣传。
寻求家人和朋友的帮助
现在,试玩可以从你的朋友、家人和其他亲密联系人开始。这限制了消除表面问题所需的时间,例如导航功能、某些方面工作不正常等。确保他们不会告诉你想听的内容。家人和朋友对你有更深入的了解,会知道你在寻找什么,所以确保他们诚实地提供反馈,并建设性地提出批评。
那些陌生人
就所有这些而言,从我们所知的人那里获得评论是很好的,因为实际上没有什么是不为人知的,你与他们交流感到舒适。但正如我们之前提到的,你需要一个你认识的人的客观意见,而且这个人除了你在测试前给他们提供的简介外,对游戏一无所知。
你希望试玩你游戏的人
考虑到所有关于人的讨论,重要的是要记住你打算为谁制作游戏。对于这一群用户,你真的需要定义你的目标受众(年龄、地点和经验水*)。通过早期开发用户档案,你能够定义他们可能具有的属性以及你需要满足的属性。这不仅将帮助你改进游戏设计,还能定义你将如何进行试玩。通过这样做,你能够集中精力提出与应用程序相关的问题,以及针对他们的问题,找出(如果尚未发现),什么可以使它更具相关性。
可以帮助你细化这一群人的简单步骤如下:
-
看看谁已经在使用你的产品(如果你有多种产品的话)。如果你还没有用户基础,那么创建一个列表,列出你最理想的用户群体。从广泛的角度开始,然后逐步细化,使其更加具体。这将有助于你以后更好地营销你的游戏。考虑诸如年龄组、性别、地点等因素,以帮助形成更清晰的画面。
-
看看与你类似的其他人在做什么。这将帮助你进一步细化(或重新定义)你的目标受众。列出一些与你的游戏相似的游戏,并看看他们的受众是如何被定位的。
-
分析你已有的产品和服务。它们是否做得很好,如果是的话为什么,如果不是的话为什么?
-
选择你受众的具体特征来集中关注。例如,虽然他们可能都是游戏玩家,但也许有些人比其他人更休闲。也许你的游戏更针对成年人而不是儿童,等等。虽然我们希望取悦每个人,但我们无法做到,所以更好的做法是更专注地定位。
-
退一步,评估你的目标受众。有没有可以添加的东西来帮助改进或明确它们?
这些只是你在定义目标受众时需要考虑的一些事情,所以我鼓励你也查看其他更相关的文本(在网络上、书籍中),这些文本对你的游戏更有相关性。
当
你决定运行游戏的时间表也是一个重要的考虑因素。特别是如果你需要人们亲自到场。在决定时间时,考虑你的小组在那个时间最可能做什么。例如,如果你的目标群体是学生,那么他们白天很可能在学校,因此,周末的测试会话可能更适合他们。另一方面,如果你的小组是那些跑步的人,那么早上或下班后可能是进行会话的理想时间。当然,这些也取决于你拥有的游戏类型。如果你的游戏需要更多的投入时间,那么给他们更长的时间来玩你的游戏将更适合你和他们的生活方式。记住,他们是在帮助你,所以灵活性是关键。
注意
确保人们按时参加你的测试小组的一个好方法是在你的日历中添加他们,或者在大约提议的游玩日期前几天打电话或发给他们提醒。
哪里
如果所有测试都是虚拟的,无论是通过下载可执行文件还是通过登录网站,那么你可以跳过这一部分。然而,如果你需要参与者到现场,你希望让它尽可能容易到达。理想情况下,你希望它在靠*公共交通的地方,并且在一个人们可能聚集的中心地带。例如,市中心通常靠*公共交通,而且可能靠*人们的办公地点或甚至学校。
什么
你对游戏测试有什么期望,以及你的测试者期望做什么,这是你需要考虑的两个问题。例如,你是否希望他们玩教程并提供给你反馈,无论是书面还是口头?也许他们认为他们需要玩完整款游戏。你的游戏测试的“什么”是至关重要的,需要定义并清楚地告知测试者,这样你就不会浪费他们的时间,他们也不会通过提供无关紧要的部分的反馈来浪费你的时间。
一点一滴,意义重大
你将在游戏测试期间为你的测试者提供什么,以及/或者为他们提供的时间?如果你无法补偿他们,确保提供食物和饮料。这并不是说你需要找一个提供蛋糕和三明治的供应商,但提供一些茶和咖啡以及一些小吃(切一些水果、薯片、糖果、饼干,甚至自己制作三明治)将有助于保持他们的清醒,尤其是如果他们待的时间很长。在长时间的游戏测试期间安排休息也很重要,因为你不想让他们坐几个小时。每 45 分钟休息一次也是一个很好的方式,可以检查他们的进展情况。
如何
测试者将如何玩耍,你将如何管理这一点?你将如何获取他们的反馈?你需要什么样的设备(录音设备或电脑)和软件(问卷)?在测试之前,确保它们工作正常,电池充满电,并且你有所有必要的设备(电缆和适配器)可用。此外,确保你有一个备用计划,以防万一在测试当天出现某些问题(这种情况确实会发生)。
游戏测试的方法
有不同的方法来进行游戏测试。你可以在提供一些背景信息后让玩家自己解决问题,或者引导他们通过规则。在每种方法中,你在游戏测试期间获取反馈的方式都会有所不同。以下是一些你可以进行游戏测试的方法。
观察
观察你的测试者是如何玩耍的。例如,他们是否以某种特定方式或出乎意料的方式做事?可能是因为一个错误或其他与游戏相关的经验,因此他们以相同的方式使用某些功能(或尝试使用)。这种概念也可能相反,例如,如果玩家应该获得或做某事,但他们没有,或者游戏不允许他们这样做,那么这需要修复。在这些点上,未来的玩家可能会因为技术问题而觉得游戏难以使用,并转向卸载按钮。
提问并解释
没有比向他人解释某事物来检验你对它的理解更好的方法了。因此,从你的试玩者那里获得反馈的一种方式是请他们向你解释游戏。他们解释的是否有所不同,让你怀疑是否给了他们正确的游戏?或者他们是否以你期望的方式解释了游戏?在他们向你解释游戏的过程中,确保你将对话保持在游戏是什么,而不是它可能是什么。当然,任何改进都是很好的建议,但为了解释的目的,你希望他们告诉你他们刚刚经历了什么。
反思并跟进
到现在为止,你面前是一个空白的测试空间,以及一大堆笔记、想法和不同的思绪在你的脑海中盘旋。现在是时候将它们整理并记录下来。召开一个小组会议。在白板上、纸上、文档上或任何容易访问的地方写下它们。这是一个重要的过程,因为这里发生的事情可以极大地影响你的游戏发布。
现在试玩已经结束,你将想要跟进他们。这就像参加会议一样,你在会议后想到了一些有用的东西,测试也是如此。在试玩一周后,给他们发一封电子邮件,再次感谢他们的参与,如果他们在试玩后想到了其他事情。这里重要的是不要通过电子邮件和问题来骚扰他们,而是给他们提供提出建议的机会。你可以提供一个在线(匿名)的调查问卷。
创建你的在线存在
现在,如果某事物不上网,它就不存在。好吧,如果你不善于社交,人们就很难知道你在忙什么,更不用说知道你在制作游戏了!你不会花几个月或几年去开发某物,然后没有人来参加派对。但这是可以解决的,这确实需要一点计划和时间的投入,所以请做好准备,但我向你保证,最终这将是值得的。
做好研究
如果你正在参加面试,你不会在没有对你潜在雇主进行一些研究的情况下进入那里,为自己树立名声也是如此。虽然吸引每个人的注意是好的,但理想情况下,你希望被正确的人注意到。做到这一点的一种方法是对与你处于同一市场的公司、产品甚至个人给予密切关注;并观察他们与谁互动,他们使用什么样的社交媒体标签。例如,他们是否使用特定的标签,如#Android、#game、#AppStore?也许,他们针对的是特定的群体,从本地、国际、大公司和小公司,甚至该地区的一些关键个人。目的是不要模仿他们的互动,而是观察他们。当你开始观察时,你会更多地了解你正在针对的市场,主要参与者,也许还会发现与他们互动的机会。这些机会可能包括会议、展览,也许是一个特定发布的见面和问候。记住,时间和地点是关键。
进行审计
从标志到横幅,链接甚至使命宣言,社交媒体审计将确保一切保持最新。在你首次设置社交媒体账户以及在你做出任何重大公告(如游戏发布)之前进行此操作。当然,定期维护这些内容很重要,但如果你要这么做,这些是检查一切是否就绪的时候,因为你很可能会获得更多流量涌入你的渠道。
与你的观众互动
如果你正在记录这个过程,请向与你的内容互动的人征求他们的意见。创建投票,发布问题并请求他们的回复。保持他们的参与度,并确保你定期与你的观众互动。即使你只是发布关于办公室一天的工作更新或游戏开发过程中发生的有趣事情,也能保持用户的兴趣。虽然发布内容是好的,但确保它以某种方式与游戏或你的开发工作室相关联。
奖励参与度
像 Kickstarter、Indiegogo 和其他众筹*台为项目支持者提供一定承诺级别的奖励。即使你选择不与这些*台之一互动,你也可以采用类似的方法来开发和最终发布你的游戏:
-
竞赛:举办竞赛是让人们参与的好方法。如果你没有资金创建产品,提供折扣、提前访问或(免费)完整版本的游戏。另一个选择是创建一些独家艺术品/壁纸,如果可能的话,让它们由你的团队打印并签名。
-
大众的力量:比赛是让人们与你的公司和产品互动的绝佳方式。因此,当你想要让人们参与进来时,与其让他们对你的问题或比赛写评论,不如让他们点赞、分享,甚至标记朋友参与比赛。也许对于他们标记的每个朋友,都是一个额外的参赛机会。无论如何,这是一个双赢的局面,因为他们(甚至他们的朋友)都有赢取东西的潜力,而你也能获得一些宣传。
-
反馈:从使用你产品的人那里获得洞察力,无论是游戏测试期间还是游戏发布后,都能确保你的游戏尽可能好。记住,当人们花时间给你提供详细的反馈时,这是他们一天中抽出时间,承诺帮助你和你产品的时刻。虽然不是所有事情都是完美的,有时你会收到负面反馈,但当它是建设性的,它可以给你关于未来更新的想法。因此,虽然反馈往往是无私的,但奖励那些付出努力的人。奖励可以是简单的折扣,也可以是更大的,比如(或赢得)免费游戏副本的机会。
社交媒体营销
不要只是做个旁观者,要参与进来!待在一边很容易,但加入其中会更好。虽然有些人可能对社交网络还不太熟悉,但这个行业已经相当融入其中,它确实在其中扮演了重要角色。然而,在你开始之前,有一些事情你应该考虑清楚,然后再大声宣扬你的工作。
写一篇关于它的博客
现在你对目标受众有了更详细的了解,是时候开始建立自己的在线存在感了。首先,让我们从博客开始。博客提供了许多途径来传达关于你所做事情的详细信息,尽管是以公司或产品开发过程中的形式。这是聚集追随者并提供更个人化生活快照的一种方式。通过这种方式,你开始与你的观众建立更有意义的联系。
有很多不同的博客*台可以选择。其中一些最受欢迎的包括 Tumblr (www.tumblr.com)、WordPress (www.wordpress.com) 和 Blogger (www.blogger.com)。这三个虽然不是唯一的,但都提供了创建你自己的博客的优秀*台。
你有没有读过一个非常吸引人的标题,让你想要继续阅读?嗯,微博*台是吸引你受众的绝佳方式,方式几乎相同。Twitter (www.twitter.com) 是一个很好的例子,因为它限制了你的字符数在 140 个或更少。所以你必须直截了当,否则你会被截断,没有人喜欢这样。
推文,在发布关于你的应用的更新时,有一些事情需要你注意,比如时区。然而,Twitter 使用了一些关键的互动方法:
-
回复:正如其名所示,它允许用户回复你所发布的内容。这让你可以与那些对你所说内容表示兴趣的人互动。
-
转发:这是 Twitter 的分享版本。所以如果有人发了一条你喜欢的推文,你可以转发它,让你的粉丝也能看到。
-
收藏:这类似于 Facebook 上的“赞”或 YouTube 上的点赞。
-
标签:在分享内容时,这是需要牢记的最基本的事情之一。它们就像一个过滤系统,用于对信息进行分类。使用独特的标签可以帮助你跟踪与你的游戏相关的内容分布,同时也使得在众多推文中被听到变得更加容易。
小贴士
确保你有一个独特的标签的好方法是使用它之前先搜索一下。
一些有用的链接,帮助你开始在 Twitter 上做广告:
练习
如果你在将内容浓缩成简短版本时遇到困难,试着给自己设定一个限制。例如,先陈述你的整个想法,然后尝试在 15 秒、10 秒和 5 秒内表达出来。你会发现,每次尝试表达你的想法时,核心概念会变得更加明显,时间越短,你的解释就会越高效、越简洁。我们常常觉得需要包含一切,认为每一条信息都同等重要,但现实中这往往只是对概念的填充。
虽然 Twitter 是一个微博*台,但 Instagram (www.instagram.com)可以通过一张简单的图片(和标题)讲述一千个字。它的工作方式与 Twitter 类似,你可以添加标签,与更广泛的受众建立联系,也许还能关注一些关键人物(可选)。
Facebook (www.facebook.com)在创建一个地方来宣传你的游戏以及与你的受众建立联系时,提供了多种不同的选项:
-
页面:在公开之前创建一个页面并在其中添加一些内容,这样你就可以在网络上建立一定的存在感。通过在页面上提供内容,给访客留下停留的理由。当他们在那里时,你需要能够让他们稍微停留一下,而且有不同方式让访客参与,例如发布一篇关于最*更新或新功能的有趣博客文章,或者展示一张引人入胜的照片。
-
帖子:这是一个多功能的特性。例如,如果你在询问关于游戏的一个特定组件,比如下一个要包含的功能,你可以让他们为选项一点赞,为选项二评论,为选项三分享。就像我们在本章讨论的竞赛一样,你可以使用选项二来鼓励人们在这个过程中标记他们的朋友。结果,当你让他们与你的帖子互动时,你也在从中获得一些宣传,同时触及他们的朋友网络。
-
投票:可以用作快速了解用户意见和查看共识的方式。它比发表评论(尽管这个功能在投票中通常是可用的)更线性,但更快。当然,你可以使用投票来鼓励竞争行为,比如“现在投票”,哪个功能获得最多的投票将包含在下一个更新中。这样,玩家也会觉得他们的意见很重要,他们也是开发过程的一部分。
-
直播:直播功能允许你实时直播一个事件,并让人们现场观看。这为你提供了直播游戏开发或问答环节等活动的绝佳机会。最好的部分是,如果人们错过了,他们可以在之后观看并与之互动(例如,通过评论、点赞、分享等)。
MailChimp
当你还没有准备好时,你仍然需要一个地方让潜在客户保持联系,而没有任何比主列表更好的方式了。MailChimp (www.mailchimp.com) 以及许多其他服务,可以帮助你不仅收集邮件列表,还可以一次性通知他们即将到来的活动、产品发布以及许多其他特别活动。
准备发布
在你最终将你宝贵的宝石发布到世界之前,有许多事情需要处理,不仅与你的游戏或应用程序有关。在接下来的部分中,我们将讨论一些考虑因素,包括你的团队和最终产品,以确保这一过程的最后部分尽可能顺利。
筹资活动
众筹活动有多个不同的*台,每个*台都有其独特的功能。然而,它们共同的基本点是它们都旨在为你提供一个聚集追随者和筹集所需资金或展示你的创造的*台。不同的众筹*台出于不同的原因运营。例如,有些是关于事业(自然、人权或贫困),某些类型的产品(服装、音乐),创意(产品或虚拟商品),以及个人(医疗治疗)。你的游戏可能符合多个标准,特别是对于创意项目,因此建议你花些时间看看你的游戏可能吸引到哪些最感兴趣的人。
众筹*台的工作方式是开发者(或任何产品或想法的创造者)展示他们的想法、他们需要资金的原因、资金的分配方式以及对于支持者(最终将给你钱的人)的好处。一般来说,你将为你产品的支持者(在这种情况下,游戏)提供不同级别的支持机会。例如,如果一个支持者捐赠了 1 美元,那么他们可以在游戏网站的信用名单上列出他们的名字,甚至可以在游戏中列出。如果他们给你 50 美元,那么他们可以获取你游戏的签名副本,800 美元将获得两份副本和一张手写的感谢信,依此类推。在设定每个级别的金额时要小心。太多的话你不会得到支持者;太少的话你会破坏你的预算。最后要考虑的一点是,在某些情况下,众筹*台不仅会从你筹集的总资金中提取一定比例,还会向支持者的捐赠中添加费用。
因此,在你决定选择众筹*台之前,请确保它将适合你,并且适合你想要筹集的资金。
最重要的事情之一是始终牢记你项目(无论是获得资助还是未获得资助)的风险。可能会发生许多风险,尤其是在你获得资助后,资金得到妥善管理和用于其预期目的。你希望有一个经过良好规划且考虑到可能发生的事件(延误、硬件的意外成本以及额外帮助)的预算。
我们只是刚刚触及了开发和运营众筹*台的意义,但这并不是一项容易的任务。当你确定支持者的奖励级别时,需要深思熟虑,并考虑其成本以及这可能会对你的预算产生的影响。例如,游戏的数字下载肯定比刻录到光盘、放在精美的盒子里,然后运送到目的地更经济。你可以将邮费包含在支持者需要支付的费用中,或者要求他们单独支付。无论如何,众筹活动需要投入大量的时间和精力才能成功进行。此外,它们并不总是获得资金的安全途径,所以在规划你的游戏及其未来时,请记住这一点。
这里有一份需要查看的众筹网站列表:
-
Fig: www.fig.co
-
Kickstarter: www.kickstarter.com
-
Indiegogo: www.indiegogo.com
-
RocketHub: www.rockethub.com
-
GoFundMe: www.gofundme.com
-
Razoo: www.razoo.com
-
CrowdRise: www.crowdrise.com
在 Unity 中构建
第一件事是实际编译游戏。否则,你将无法将其放置在任何地方!
要在 Unity 中构建游戏,你需要在顶部菜单栏中导航并选择文件 | 构建设置…。在那里,你有以下屏幕,你可以选择你想要的目标*台(以及你想要包含的场景):

如果你点击玩家设置…按钮,你将进入构建的具体选项。在这里,你可以找到更多关于它的信息:docs.unity3d.com/Manual/class-PlayerSettings.html。
一旦你点击构建,Unity 实际上会构建游戏,你可以喘口气(并且休息一下)。但在这里,这只是第二阶段的开始,在这个阶段,你需要实际发布和推广游戏!
澄清事实
你已经兴奋起来,你几乎到达了终点线,但在这个时候确保每个人,我是指所有参与其中的人,都清楚接下来会发生什么,这是至关重要的。更重要的是,确保每个人都已经被计算在内。例如,利润/版税如何分配,游戏发布后的责任,等等。当然,这些内容在项目早期就已经在合同中概述,他们已经签署了,但我们都有在实际上没有阅读的情况下同意条款和条件的过失。在这个阶段提醒大家重新审视他们的合同甚至可能是有益的。
接受条款和条件
现在既然你的团队都准备好了,你需要确保你即将向世界展示的内容也遵守你将要针对的*台条款和条件。例如,如果你计划为 Android 设备发布,确保你已经满足了他们的条款和条件;对于苹果的 App Store 以及你打算发布的任何其他*台也是如此。
本地化
当你面向全球发布时,你需要本地化你的游戏。许多开发者认为仅用英语发布应该足够,因为大多数玩家都能理解英语。然而,如果你查看市场和下载图表,很明显英语只占市场的 20%。因此,你绝对应该考虑将你的游戏本地化到其他受众。
但本地化远不止于将你的游戏翻译成其他语言或添加字幕。有许多事情你应该考虑。以下是一些众多原因中的几个:
-
在某些语言中,如德语,一些单词非常长,它们可能会被截断或从用户界面溢出。
-
一些语言,如阿拉伯语,是以不同的方向书写的,例如从右到左或从上到下。因此,你可能需要调整你的 UI 设计以支持界面将产生的视觉影响。
-
如果你拥有对话系统,或者代码中使用字符串连接的用户界面,请注意,在其他语言中,如果它们不够灵活,它们需要重写!一个例子是当你想在单词前放置一个形容词时。在其他一些语言中,它可能发生在相反的方向。例如,英语句子“the red hat”在意大利语中变为“il cappello rosso”,其中“rosso”意为红色。因此,形容词在单词之后而不是之前,这与英语不同。另一个(实用的)例子如下:你为你的物品预留一个额外的字符在末尾以添加一个“s”使其变为复数(“potion”可以变为“potions”)。在意大利语中,你不需要额外的字符,只需更改最后一个字符(“pozione”变为复数形式“pozioni”)。此外,在意大利语中,最后一个更改的字符取决于对象是女性还是男性(这是一个在英语中不存在的概念,因为对象没有性别)。因此,你为预留字符的解决方案结构不足以支持意大利语。想象一下将这种解决方案应用到所有其他语言,你很快就会意识到仅通过添加几个变量来本地化你的游戏并不容易。
-
在某些文化中,数字中逗号和点的角色是颠倒的(例如,数字 3,218 可以解释为三千二百一十八,也可以解释为三点二一八,就像在意大利一样)。
-
在某些文化中,某些概念可能不受欢迎,因此应该被审查或删除。这通常与暴力游戏或包含某些类型成人内容的游戏有关。
在翻译时,你应该考虑所有这些问题,因为翻译并不像获取文本的翻译那么简单。以下是一些例子,但绝非详尽无遗:
-
翻译人员需要了解游戏背景,因为在其他语言中,直接翻译会丢失很多细微差别,在可能的情况下,它们应该被保留。
-
角色的传记在合作者为其他语言配音时很有用
-
一些隐喻或谚语在其他语言中可能没有意义
因此,你应该从开发流程的最初阶段就考虑本地化。然而,由于时间和经济限制,这通常是不可能的。无论如何,请记住,你越早关注本地化,长远来看就越容易。
伦理考量
电子游戏是一种技术,就像所有技术一样,它们可能是有害的,会伤害人们。它们可以被用作伤害人的武器。不幸的是,游戏的这一伦理方面往往被忽视。因此,我想引用 Jesse Schell 在他的书中(《游戏设计艺术:透镜之书》)所写的内容:
"如果你正在设计一款涉及陌生人交谈的游戏,你必须对可能导致的后果负责。这是罕见的情况之一,你的游戏设计选择可能会拯救或失去生命。你可能认为你的游戏中发生危险的可能性只有百万分之一,但如果这是真的,并且你的游戏非常成功,有五百万人在玩,那么那个危险的事情就会发生五次。"
作为游戏开发者,你有责任尽你所能确保这种情况永远不会发生。如果你不愿意承担这样的责任,可能最好不要制作任何游戏。
在我个人的观点中,这也与 Jesse Schell 的观点一致,正如你可以从他的书中读到的那样,你有潜力做一些好事,并利用电子游戏作为提升人类生活的工具。你应该被像“我正在开发的游戏是否在做一些好事?”和“我的游戏能否以某种方式改善玩家的生活?”这样的问题所激励。当然,这些问题并不是游戏公司关心的,但你作为游戏开发者应该关心,并在内心深处回答这些问题。然后,尝试让你的游戏成为对人们更好的游戏。
注意
同样的论点,但解释得更详细,可以在 Jesse Schell 的书中找到专门的章节。
请以你自己的方式,让这个世界变得更美好!
摘要
在这一章中,我们遇到了许多主题,探索了许多领域,所以让我们重新组织我们的想法。
在开始时,我们讨论了我们塔防游戏的潜在改进以及它们实施的一些提示。从那里,我们将改进扩展到了 Unity 的整体,通过提供更多我们没有时间详细覆盖的游戏开发领域,但如果你希望提高自己的技能,这些领域需要你的关注。
在游戏开发过程中,你通常并不孤单,而是身处一个团队中,并且每个团队成员都像是一个整体(就像人体一样)那样工作,这对于实现最佳结果非常重要。因此,我们强调了某些协作工具,这样你就可以自由尝试并选择最适合你团队的工具。
然后,我们回到了我们的游戏,通过关注优化和测试,这两者都被错误地认为是游戏开发流程的最后步骤,但正如我们所发现的那样,这并不正确。你越早开始迭代它们,你的游戏就会越好。优化是使你的游戏高效运行所必需的,而测试是使玩家的体验流畅所必需的。
我们并没有就此停止,而是继续讨论了游戏完成后的事情,我们讨论了从营销你的游戏、创建在线存在感以发布你的游戏,以及希望从中获得收益的话题。因此,我们探索了许多关于如何利用最常见的社会媒体*台作为工具来通过参与和扩大目标受众来推广你的形象和你的游戏的提示。
最后,我们只是触及了本地化的表面,只是为了更好地了解其背后的内容,因为我们没有时间正确面对它,但这一点值得一提。
最后的笔记和告别
不幸的是,这次在游戏开发世界的旅行已经结束(尽管最后一章应该让你意识到这仅仅是你的旅程的开始)。时间飞逝,我刚刚意识到我填满了一整本书!
我真的非常感谢你们所有人,那些我没有机会亲自见到的读者。但当我写到这本书的最后一行时,我感到以某种方式与你们所有人都有联系。对我来说,这是一次令人惊叹的旅行(尽管有时会感到疲惫和有压力)。
如果你非法下载了这本书但真的很喜欢它,请考虑购买它。我努力提供高质量的内容,这需要时间。如果你负担不起这本书,你总是可以给我一杯咖啡(www.francescosapio.com/BuyMeACoffee),或者通过在社交媒体上分享购买链接(而不是通过种子和下载)来支持这本书。
我还写了其他可能对你旅程有所帮助的书籍,包括 Unity UI 美术设计手册,Packt 出版 (www.packtpub.com/game-development/unity-ui-cookbook) 和 Unity 5.x 2D 游戏开发蓝图,Packt 出版 (www.packtpub.com/game-development/unity-5x-2d-game-development-blueprints)). 此外,这里还有我在 第一章 中引用的迷你指南,Unity 中的*坦世界:你需要了解的 Unity 5 知识,Packt 出版 (www.packtpub.com/packt/free-ebook/what-you-need-know-about-unity-5)。
最后,网络交流、结识他人和分享想法是扩展你的知识和技能的绝佳方式。所以请随时联系我:























浙公网安备 33010602011771号