Unity-认证程序员备考指南第二版-全-
Unity 认证程序员备考指南第二版(全)
原文:
zh.annas-archive.org/md5/dada881490221e492773464f21c04884译者:飞龙
前言
Unity 认证程序员:考试指南 – 第二版将从一个基本的面向对象程序员开始,通过贯穿整个书籍的创意项目,介绍 Unity,实现考试的核心目标,这些目标可以用于 Unity 自己的官方程序员考试。
本书将带你(程序员)讨论考试本身,分解每个目标,以及你需要达到通过考试的标准。从那里,我们将讨论的一切都将与你从考试中可能遇到的问题有关。因此,我们将立即开始,并参考常见设计模式的概述以及所有程序员都需要了解的更多常见 SOLID 原则。在我们接触 Unity 之前,我们将先通过我们的游戏设计简报和自定义框架进行讨论。
安装 Unity 后,你将开始构建横版射击游戏的第一个步骤,并且在每个章节的开始,都会提醒你我们将要覆盖的核心目标以支持你。在几个章节之后,你还将通过一个迷你模拟考试来测试你的进展情况。
到本书结束时,你将创建一个可以在独立 PC 和/或支持键盘和触摸屏控制的 Android 设备上玩的游戏,你将驾驶宇宙飞船来对抗迎面而来的敌人。
早在第二章,添加和操作对象,你将已经编写了游戏的大部分代码,接下来我们将逐步介绍的章节将向你介绍 Unity 的工具和组件,例如专为电视/电影行业和游戏中的场景设计的 Timeline。你将涵盖粒子效果、应用于游戏对象的不同材质以使它们对光线做出反应、通过脚本操作音频混音器来淡入淡出声音、暂停游戏、在自定义脚本对象中存储值以及更多更多。
即使你没有参加 Unity 认证程序员考试,你也将制作一个可以继续开发、游玩和学习的游戏。
本书面向的对象
这本 Unity 引擎书籍是为游戏开发者、软件开发者、移动应用开发者和希望在其职业生涯中取得进步并获得游戏行业认证的 Unity 开发者所写。本书假设读者具备基本的 C#编程和 Unity 引擎知识。
本书涵盖的内容
第一章**,设置和构建我们的项目,介绍了考试中对你的期望,讨论了 SOLID 原则,并概述了设计模式。你还将通过查看其框架和版本控制来了解我们如何创建我们的游戏。
第二章,添加和操作对象,将帮助你开始编码和导入 3D 资产,以获得游戏基本功能的基础。
第三章,管理脚本和进行模拟测试,将游戏扩展到菜单屏幕,添加声音,添加评分系统,并以第一次模拟考试结束。
第四章,应用艺术、动画和粒子,专注于理解材质、动画纹理以及创建粒子系统。
第五章,为我们的游戏创建商店场景,介绍了商店场景以及如何使用 Unity 的射线投射系统,该系统发射不可见的射线以帮助识别游戏对象,并探讨了各种可脚本化对象的使用,以填充内容。
第六章,购买游戏内物品和广告,涵盖了使商店场景具有工作状态的游戏内平衡,以便购买升级,并介绍用户观看广告以获得额外的游戏内积分。到本章结束时,玩家将能够使用新武器进行射击,并从购买的盾牌中承受额外的敌人攻击。
第七章,创建游戏循环和模拟测试,涵盖了在屏幕间移动直到游戏循环回到开始以创建游戏循环,并以一个包含对已学材料的模拟测试问题的模拟测试结束。
第八章,添加自定义字体和 UI,使您更熟悉 Unity 的 2D 画布,通过应用图像组件和自定义字体以及动画每个级别的标题来为每个屏幕添加润色。
第九章,创建 2D 商店界面和游戏内 HUD,将商店场景从看起来更像原型转变为更加精致和功能化的界面,以支持各种屏幕宽高比。本章还介绍了游戏内的生命、地图和评分系统。
第十章,暂停游戏、更改音效和模拟测试,涵盖了为游戏每个级别创建暂停屏幕,这将提供更改游戏音量控制选项,以及退出和恢复游戏,之后是一个模拟测试,以检查您对本章知识的掌握。
第十一章,存储数据和音频混音器,利用 Unity 自带的 PlayerPrefs,并将其与 JSON 以及使用远程设置在云端存储数据进行比较。
第十二章,NavMesh、时间轴和模拟测试,介绍了一种新敌人,它试图使用 AI 逃离玩家,并探讨了使用 Unity 的动画工具时间轴将 Boss 动画引入场景,并扩展其功能以动画闪烁的灯光。本章末尾还有一个模拟测试,以查看事情进展得如何。
第十三章,效果、测试、性能和替代控制,讨论了利用碰撞体、rigidbody属性、视觉效果后处理、全局照明和反射探针。本章还探讨了进一步的游戏功能以支持移动控制,以及如何在 PC 和移动设备上构建和测试游戏。
第十四章,完整的 Unity 程序员模拟考试,包含超过 90 个问题,用于测试您从所有 13 章中学到的内容。答案可以在附录中找到。
为了充分利用本书
对 Unity 有一定的了解会有帮助,但不是必需的。需要具备 C#或任何其他面向对象编程知识的基本理解。在撰写本书时,Unity 考试基于 Unity 2020 LTS 版本。我们将在第一章,设置和构建我们的项目中介绍下载和安装软件的流程。如果你出于任何原因使用 Unity 的较新版本,那不应该有问题,除非本书提到版本之间可能存在差异。
系统要求
以下表格列出了 Unity 在您的系统上运行的要求。有关系统要求的更多信息,请查看以下链接:docs.unity3d.com/Manual/system-requirements.html。

下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition。如果代码有更新,它将在 GitHub 仓库中更新。
我们还提供了来自我们丰富的书籍和视频目录中的其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!
代码实战
本书的相关代码实战视频可以在bit.ly/3LslyB0查看。
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表的彩色图像 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781803246215_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“Unity 包是一个包含各种可以在 Unity 中使用的资源的单个文件,其方式类似于.zip文件。”
代码块设置如下:
void Start()
{
this.transform.localPosition = Vector3.zero; startPos = transform.position;
Distance();
}
从层次结构窗口中选择GameManager游戏对象,然后转到检查器窗口。
小贴士或重要注意事项
看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件联系我们 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/errata 并填写表格。
盗版:如果您在互联网上遇到我们作品的任何形式的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件联系我们 copyright@packt.com 并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了 Unity Certified Programmer Exam Guide Second Edition,我们很乐意听到您的想法!请点击此处直接进入此书的 Amazon 评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
第一章:第一章:设置和构建我们的项目
在一段时间内,Unity 一直在发布涵盖不同技能的考试,这些技能适用于毕业生、自学成才者或在其领域中被归类为资深人士的人。
如果我们在 Unity 网站上检查先决条件(unity.com/products/unity-certifications/professional-programmer),他们会告诉我们,这项考试不是为绝对初学者准备的,你需要至少 2 年的 Unity 和计算机编程经验,包括 C#。这本书将带你通过熟悉 Unity 及其服务的过程,直到可能感觉像是一门入门课程;然而,我期望你知道 C#编程的基础知识,比如if语句是什么,函数的作用,以及类代表什么。如果你不知道,我建议你首先阅读 Harrison Ferrone 的《通过 Unity 2020 开发游戏学习 C#》(www.packtpub.com/product/learning-c-by-developing-games-with-unity-2020-fifth-edition/9781800207806)这本书。请注意,这项考试基于 Unity 2020 LTS。这是 Unity 专业程序员考试的第二个版本。如果你跟随这本书安装 Unity 2021 及以上版本,你将不会获得任何实际的好处。相反,你可能会因为编辑器和一般功能可能不同而与这本书的学习过程脱节。
如您所想,有时很难评估一个程序员的水平。想象一下雇主招聘员工的情况。通常,程序员会根据他们的作品集来评判,但如果你是一个没有作品集的毕业生,或者因为你忙于学习而缺乏大量工作,会发生什么?也许你已经做了多年的程序员,但由于签署了保密协议,无法展示任何最近的工作?一些雇主可能会查看你的简历,甚至不查看你的作品集,因为资格看起来并不那么令人印象深刻。潜在雇主可以给开发者进行的测试也可能是不平衡的、不公平的、不切实际的,而且挑战性不足;很可能是雇主从互联网上抓取了一个程序员的问卷调查模板来测试你。
然而,从 Unity 本身获得资格,清楚地表明你已经接受了测试,并覆盖了所有认可你为认证 Unity 程序员的领域。即使你有一个相当不错的作品集,展示了标准化和专注的水平,拥有 Unity 的资格也能在求职申请中给你带来优势。
本书有两个主要目的:
-
带您通过一个有趣、简单、横向卷轴射击项目,该项目包含可下载的艺术资源和声音,将涵盖 Unity 考试的核心目标
-
通过定期的测试和复习,尽可能让你为考试做好准备
因此,如果你觉得你不需要执行项目,请跳到本书的最后一部分尝试最终的模拟测试——实际上,我建议你现在就做。翻到书的背面,参加测试,如果你没有达到你计划的成绩(即得分超过 75%),至少你知道你还有东西要学,完成项目可能会有所帮助。如果你对模拟测试的成绩不满意,不要立即参加考试——你将面对的是自己的肌肉记忆,而不是知识本身。
Unity 将本考试的必要领域划分为六个核心目标。在本章中,我们将在介绍我们的侧滚动射击项目之前,介绍这些目标是什么,该项目将涵盖大多数目标。我们还将在本书的附录部分介绍项目之外的专业主题,例如网络、VR 等。
在接下来的章节中,我们将通过编码的一般实践来刷新自己——有点像在项目编码时的“该做和不该做”。然后,我们将了解游戏类型,并希望让你思考如何设置游戏框架。最后,我们将下载并设置我们的空项目在 Unity 中,并了解 Unity 服务。
在本章中,我们将涵盖以下主题:
-
六个核心目标
-
设计模式的概述
-
SOLID 原则
-
设计 Killer Wave 游戏
-
Killer Wave 游戏框架
-
设置 Unity 2020 LTS
-
设置你的 Unity 项目
本章我们不会进行任何编码,因为我们的重点是 Unity 在考试中希望从你那里得到的内容。我们将讨论方法论的概述和与设计模式相关的代码结构。你可能想跳过一些部分,因为你根本不感兴趣,但请记住,我之所以提到大多数这些东西,是因为它们很可能在考试中出现。所以,请不要觉得我是故意惩罚你!
下一个部分将详细介绍本章涵盖的核心目标。
本章涵盖了核心的考试技巧
在专业软件开发团队中工作:
-
设置你的 Unity 项目
-
识别用于构建模块化、可读性和可重用性的脚本结构技术。
技术要求
查看以下视频以查看代码的实际应用:bit.ly/3klZRqf。
六个核心目标
考试将主要关注脚本编写和 Unity 的 应用程序编程接口 (API)、动画控制器、粒子、渲染等的使用。整个想法是让你熟悉 Unity 作为程序员所能提供的内容。Unity 将他们的考试划分为核心部分,这是一种很好的方式,可以分离考试的工作量。
六个核心目标如下:
-
编程核心交互
-
在艺术管线中工作
-
开发应用程序系统
-
编程用于场景和环境设计
-
优化性能和平台
-
在专业软件开发团队中工作
让我们更详细地看看这些内容。
编程核心交互
当我们在 Unity 中加载第一个空白场景时,我们将控制对象(或者,正如 Unity 喜欢称呼的,游戏对象),移动、旋转和/或扩展它们。你不仅可以调整或转换这些游戏对象,还可以为它们添加组件,使它们表现得像摄像机、灯光和/或动画片段等。每个组件通常都会有属性和值。例如,摄像机组件将会有调整其视野、背景颜色和其他一些与摄像机相关属性的属性。另一个组件的例子是刚体。刚体组件通常用于当你想要两个游戏对象之间发生碰撞时。或者当游戏对象接触另一个游戏对象时,你希望它做什么?它会爆炸吗?它会收集另一个游戏对象吗?它会将其推出,因为有一个更大的力量?Unity 希望你知道如何使用这些组件和游戏对象。他们还希望你知道如何使用控制板或键盘控制来控制这些对象,就像它们是电脑游戏中的角色一样。这听起来可能已经令人畏惧,但你不需要成为数学老师就能成功(但如果你是那就太好了!)。Unity 的出色之处在于它为你做了很多艰苦的工作。你所需要知道的只是你想要什么以及你想要如何使用它。
为了通过考试,你需要知道以下内容:
-
实现和配置游戏对象行为和物理
-
实现和配置输入和控件
-
实现和配置摄像机视图和移动
让我们继续到第二个 Unity 考试核心目标——艺术。
在艺术管线中工作
如你所知,这是一场编程考试,那么为什么我们的考试目标中会提到艺术呢?好吧,作为一个程序员,你很可能需要操作游戏对象来完成考试目标中提到的事情。你可能不只是移动某个东西——你可能还想要改变游戏对象的颜色。例如,与其拥有一个单调、平面的汽车游戏对象,你可能会想要它闪闪发光、具有金色色调。为了实现这一点,游戏对象通常会被分配一个材料,你可以应用贴图。这些贴图中可以包含颜色、标记和凹痕。
所有这些地图及其属性都将改变、更改或增强你的游戏对象,使其成为所谓的管道,这是你的游戏对象变成比其原始形式更多的事情的过程。如果你想让游戏对象的汽车轮子转动,你该如何做到这一点?你可能没有动画师来做这件事。你也可能被要求在代码中动画化场景的照明,而不是手动调整其属性。你不需要成为动画或照明的专家,但 Unity 希望你了解基础知识。可能不是艺术家的工作是在你的游戏中包含雪或雨,你很可能会使用粒子系统来创建这些效果。你将如何更改其属性,在代码中将它从轻微的细雨变为雷暴?如果你不知道,不要担心——你很快就会介绍这些组件及其属性。
为了通过考试,你还需要知道如何做以下事情:
-
理解材质、纹理和着色器,并编写与 Unity 渲染 API 交互的脚本
-
理解照明,并编写与 Unity 照明 API 交互的脚本
-
理解二维和三维动画,并编写与 Unity 动画 API 交互的脚本
-
理解粒子系统和效果,并编写与 Unity 粒子系统 API 交互的脚本
现在我们来谈谈第三个 Unity 考试核心目标,我们将重点关注接口、存储数据和了解多人游戏功能。
开发应用程序系统
我不会说这是一个核心目标,这更像是一系列 Unity 将其捆绑在一起并标记为“核心”的事情。因此,让我们将其分解,弄清楚他们希望我们从哪里得到。开发应用程序系统关注 Unity 如何与用户通信并存储他们的信息。这就是为什么用户界面(UI)需要包含正确的指导和信息;但从技术角度来看,无论屏幕尺寸比例如何,它都需要正确定位。UI 也可以在游戏中以最小图的形式使用,引导玩家通过迷宫,显示敌人位置。UI 还可以用于在线广告和显示来自不同计算机服务器的信息。当从玩家那里获取信息时,这些信息有多敏感?是否应该以低安全性存储在本地?我们需要加密吗?是否应该以不同的文件格式存储在网络上?最后,Unity 目前正在淘汰他们的多人网络系统,称为 UNet,并替换为全新的系统。这意味着我们只需要了解 Unity 的网络并准备一些通用的网络考试问题。
为了通过考试,你需要知道如何做以下事情:
-
解释应用界面流程的脚本,例如菜单系统、UI 导航和应用设置
-
解释用户控制的定制脚本,例如角色创建器、库存、店面和在应用内购买
-
通过利用 Unity Analytics 和 PlayerPrefs 等技术,分析用户进度功能脚本,例如得分、等级和游戏内经济
-
分析二维叠加脚本,例如抬头显示(HUDs)、小地图和广告
-
识别保存和检索应用程序和用户数据的脚本
-
识别和评估网络和多玩家功能的影响
让我们继续到第四个 Unity 考试核心目标,我们将再次关注游戏对象。
场景和环境设计的编程
这个核心考试目标听起来与第一个核心目标相似,我们在那里介绍了游戏对象;然而,这次我们更专注于对象的管理。游戏对象何时被创建?它是如何被创建的?当我们不再需要它时,我们如何处理它?我们应该销毁它吗?或者我们将其标记为已销毁,但在场景的另一个地方存储以节省内存?我们还可以看看一些常见的组件,例如用于人工智能的 Nav Mesh Agent,了解如果它是一个知道何时巡逻、追击敌人或隐藏的角色,游戏对象会做什么。我们还需要了解音频组件和混音器,我们如何操作它们,以及如何创建回声效果。再次,我们面临的情况与动画和艺术一样——我们不需要在这些技能上表现出色,我们只需要知道它们存在。
要通过考试,你需要知道以下内容:
-
确定实现音频资源的脚本
-
识别实现游戏对象实例化、销毁和管理的方法
-
确定使用 Unity 导航系统进行路径查找的脚本
让我们继续到第五个 Unity 考试核心目标,这是关于当你破坏了某些东西时该做什么以及如何检查性能问题。
优化性能和平台
任何程序员都会遇到问题,在解决问题之前了解问题有时是有帮助的。这个 Unity 考试的核心目标是关于跟踪和修复你自己的问题。有时,你需要逐步检查你的代码以找到破坏游戏的 bug,或者你可能想知道为什么当游戏在某个特定点播放时会出现卡顿。这就是你将使用 Unity 的一些实用工具,如分析器,来监控性能的时候。你将能够剥离组件,以确定你是在处理物理问题,还是你的第二个场景加载时间过长,例如。能够使用 Unity 的工具解决自己的问题是这个核心目标的关键点。Unity 希望你考虑的其他问题示例包括,例如,如果你要构建一个虚拟现实应用程序,UI 将放在哪里,如果有的话?你是否需要更加关注你的每秒帧数?这些是我们将在书中讨论的问题类型。
为了通过考试,你需要了解以下内容:
-
使用 Unity 分析器等工具评估错误和性能问题。
-
识别针对特定构建平台和/或硬件配置的优化需求。
-
确定 XR 平台上的常见 UI 功能和优化。
现在我们继续讨论你的第六个也是最后一个 Unity 考试核心目标,与人合作。
在专业软件开发团队中工作
在专业环境中与他人合作,并共享和协作他人的代码可能会很棘手,如果缺乏合理的结构。版本控制等工具可以帮助,其中每个成员都可以将他们的工作“推送”到 Unity 的服务器(或通常所说的云)上,以便其他人可以共享并从中工作。一些用户可能远程工作,或者你们所有人都可以远程工作。存在不同类型的版本控制;最典型的一个被称为git。
为了通过考试,你需要了解以下内容:
-
展示对开发者测试及其对软件开发过程影响的了解,包括 Unity 分析器和传统的调试和测试技术
-
认识到用于构建模块化、可读性和可重用性的脚本结构技术
就这样!如果你知道这六个核心目标的内容,你将能够通过考试。本书将涵盖我们在本章后面将要讨论的项目中的所有这些问题和挑战。你将如何知道你是否成功地达到了你的目标?我将在每几章中提出问题,看看你的进展如何。如果你失败了或者表现不佳,那么我认为这是一个好事,因为这将让你清楚地知道你需要专注于哪些内容,并在考试前重新审视。
总之,这些都将在后续内容中展开。接下来,我想谈谈设计模式。鉴于我们正在编码,讨论代码结构、遵循合理的方法以及在没有足够规划的情况下不会陷入混乱的代码是很合适的。
设计模式概述
在本书的开头,我提到我将尽可能多地涵盖 Unity,即使预期你在参加考试之前已经使用了至少 2 年的 Unity。至于编程的基础知识,我们将显然应用 C#代码。因此,我期望你对函数、方法、if语句、类、继承、多态等事物熟悉。我将解释我在代码中做了什么,以及你应该对每一块代码做什么,但不会讲解代码每个部分的原理。
设计模式是解决你可能会遇到的问题的典型解决方案,如果你有一个可以解决问题的模式,那么你应该使用它。自己创建应用程序,使用自己的工作流程是很好的,但如果你能用设计模式的术语向另一位程序员解释一个问题,这表明你知道你在说什么;如果他们是优秀的程序员,他们很可能也明白你在说什么。你了解的模式越多,你的代码就越灵活和标准化,你很可能需要不止一个模式。否则,你可能会强迫你的代码适应一个可能不适合它的结构,这只会造成问题。
被认为是所有模式基础的 23 个设计模式是由四人帮创建的。如果你想了解“四人帮”是谁以及他们的所有 23 个模式,请访问www.packtpub.com/gb/application-development/hands-design-patterns-c-and-net-core。所有这些模式被分为三类——创建型、结构型和行为型:
-
创建型:这些模式旨在处理对象的创建——对象是如何以及在哪里被创建的。
-
结构型:这些模式旨在展示实体之间的关系。
-
行为型:这些模式旨在处理对象之间的通信方式。
理想情况下,在你通过考试后,尽量多地去了解更多的设计模式。习惯它们,因为这将有助于你未来的角色。如果你对学习更多关于 C# 和设计模式感兴趣,我推荐阅读 《动手实践 C# 和 .NET Core 设计模式》 (www.packtpub.com/application-development/hands-design-patterns-c-and-net-core)。这本书不是基于 Unity,而是基于 C# .NET core,这意味着它包含了与 Unity 重叠的编码库。Unity 包含了 .NET 的元素,而你作为程序员越高级,不可避免地会开始深入研究 .NET。然而,这超出了本书的范围。让我们回到我们对可能在考试中问到的一些设计模式的概述。第一个模式是 Builder,让我们来看看它。
Builder
四大设计模式中的第一个是 Builder 设计模式。这种设计通常用于对对象进行修改。如果你能想象一系列规格来创建一个定制的飞船,我们可能(或不)需要窗户、推进器、机翼、激光、导弹以及其他任何东西。将这些放入代码中可能会变得过于复杂,最终得到一些糟糕的东西,比如构造函数中的参数列表。
以下代码是一个例子,展示了一个构建得不好的飞船,其实例中包含了许多膨胀的参数:
SpaceShip spaceShipWithGuns = new SpaceShip(true, false, null, null, 8, true, null);
另一个噩梦可能是一系列子类,其中每个可能的组合都为每个飞船制作:
public class SpaceShipWithBoosters : SpaceShip
{
//Making a ship with boosters…
}
public class SpaceShipWithOneBoosterAndAWindow : SpaceShip
{
//Making a ship with one booster and a window…
}
我们都不希望做这两个例子。我们的代码将会膨胀,从长远来看,将对我们或任何其他人来说都更难以维护。
Builder 设计模式通常包含一个接口,其中包含你飞船的所有不同部分(助推器、武器等)的方法。
当涉及到制作飞船时,我们将有一个类,它将继承接口的方法,每个方法都可以接受值来指定飞船是否有导弹或窗户,以及如果有,有多少个。
以下图表显示了 Builder 设计模式的工作原理:

图 1.1 – 构建者设计模式
更多信息
如果你想要了解更多关于 Builder 模式的代码信息,你可以访问以下链接并安装一个在 Unity 中运行的代码演示:
github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Patterns
现在我们继续到下一个设计模式——Singleton,在这个模式中,我们可以有一个控制点,大多数代码从这个点发送和接收数据。
Singleton
单例(Singleton)设计模式实际上并不是一个模式,而更像是一种程序员爱恨交加的常见实践!我很快就会解释原因。
单例(Singleton)模式充当代码可能来自和前往的核心位置。我们对单例(Singleton)的主要想法是只有一个实例,例如,一个游戏管理脚本、一个关卡管理脚本、一个音频管理器:不是相同实例的多个副本。这些类型的脚本将存在于你的 Unity 场景中,并且可能永远不会被移除。如果它们被移除或不存在,那么只有一个实例会被实例化。你可以使用单例(Singleton)模式为管理类型对象,它可以监督游戏,或者它可以保存玩家所在的关卡、关卡剩余时间、在这个关卡中将使用的敌人类型等等。这是一个游戏不希望忘记或拥有多个版本的中心通用点。像所有设计模式一样,它之所以被称为单例(Singleton),是因为应该只有一个其实例。
因此,这似乎是一个很好的设计模式。然而,有人认为单例(Singleton)模式对项目代码的其他部分控制过多,也可能危害其他设计模式,特别是如果你有一个依赖于特定顺序的系统。它还违反了 SOLID 原则——关于代码应该如何被对待的指南,我将在本章后面讨论。简而言之,单一职责原则意味着脚本不应该包含比它最初构建时更多的内容。正如你可以想象的那样,单例(Singleton)模式可以很容易地变得复杂,因为它承担了多个职责。设计模式的成功在很大程度上取决于设计师的舒适度;它还取决于项目的要求。无论如何,单例(Singleton)仍然是一个流行的模式,我们将在本项目中使用它。
回到单例(Singleton)的定义,我们可以将其描述为一个确保类只有一个实例并提供全局访问点的模式。
以下图表显示了单例(Singleton)设计模式的工作原理:
![Figure 1.2 – Singleton 设计模式]
![Figure 1.02_B18381.jpg]
Figure 1.2 – Singleton 设计模式
更多信息
如果你想要了解更多关于单例(Singleton)模式及其代码的信息,你可以访问以下链接并安装 Unity 中运行的代码演示:
github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Patterns
让我们继续下一个设计模式,抽象工厂(Abstract Factory),它专注于创建一个可以后来添加额外特性的通用特性模板。
抽象工厂
抽象工厂模式旨在涵盖多个对象共享的常见特性。例如,如果我想让敌人攻击玩家,我希望所有敌人都有一个生命条,并且我也希望它们能够从玩家那里受到伤害。使用抽象工厂,我可以创建一个模具,无论创建什么敌人,它们都将具有这两个属性,而不是每次为每个敌人单独创建它们。这种设计使得项目中的事情更加容易和统一。
以下图表显示了抽象工厂设计模式的工作原理:


图 1.3 – 抽象工厂设计模式
更多信息
如果你想了解更多关于抽象工厂模式及其代码的信息,你可以访问以下链接并安装一个在 Unity 中运行的代码演示:
下一个设计模式是原型。这个模式对于创建现有对象的克隆非常有用。
原型
这是一个另一个简单的模式,它和抽象工厂模式有一些相似之处,但这个模式创建的是它所附加对象的克隆。因此,这更像是一个自我创建的链环,而不是一个工厂。从另一个角度来看,不深入细节的话,它模仿了 Unity 自己的预制系统(docs.unity3d.com/Manual/Prefabs.html)。使用 Unity 的预制系统,你可以拖放一个游戏对象来实例化另一个对象。与原型的区别在于,这是通过代码实现的,因此,如果我们愿意,我们可以添加更多的代码来使这个模式比仅仅实例化一个对象更加智能。
使用这个设计模式的一个很好的例子是在游戏中使用敌人生成器。如果我们有一支小军队的相同敌人从相同点冲向玩家,那么这将非常有效。
以下图表显示了原型设计模式的工作原理:


图 1.4 – 原型设计模式
更多信息
如果你想了解更多关于原型模式及其代码的信息,你可以访问以下链接并安装一个在 Unity 中运行的代码演示:
让我们继续到下一个设计模式——对象池,这次它不是来自四人帮,但值得提及,因为它很常见,并且在处理大量游戏对象以节省系统资源时应该实现。
对象池
这个设计模式更像是一个良好的实践工具,而不是一个真正的设计模式;然而,它被认可为一种。让我们通过一个例子来解释。
想象你正在为移动设备开发一款游戏,并且希望你的游戏能够支持尽可能多的移动设备类型,甚至包括那些性能并不强大的旧手机。你的游戏中有许多子弹在屏幕上发射。一种典型的发射子弹的方式是实例化它们,当子弹离开屏幕或击中敌人时,子弹播放爆炸动画,发出声音,然后销毁自己,因为不再需要它了。这适用于每颗发射的子弹。好吧,如果告诉你总共只需要 10 颗子弹,而且这些子弹都不会被销毁,你会怎么想?这就是 对象池 的理念;子弹可以放在游戏视图之外,玩家看不到它们。当玩家发射子弹时,第一颗子弹被移动到玩家旁边,然后当子弹接触时,它播放爆炸动画,发出声音,并与其他九颗子弹一起离开屏幕。这节省了移动设备的资源,因为它一次只处理 10 颗子弹,或者玩家在屏幕上可以发射的子弹数量。
以下图表展示了 对象池 设计模式的工作原理:

![img/Figure_1.05_B18381.jpg]
图 1.5 – 对象池设计模式
更多信息
如果你想了解更多关于对象池的代码信息,你可以访问以下链接并安装一个在 Unity 中运行的代码演示:
github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Patterns
让我们继续到最后一个设计模式,这个模式也不是来自四人帮,但同样足够常见,值得讨论。
依赖注入
这个模式通常在通用的 C#应用程序和网站开发中实现,在这些应用中,你可以选择使用构造函数来设置每个类。Unity 并不喜欢使用这些构造函数(docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/constructors),因为它是从继承 monobehaviour(每个新创建的 Unity 脚本都会自动带有)中来的(更多信息请参阅docs.unity3d.com/ScriptReference/MonoBehaviour.html)。当然,你可以移除 monobehavior,但这样你将失去 Unity 的大量功能。但依赖注入的目的是,你可以有执行不同任务的类,因为它们已经从抽象中接收到了数据。
我在考试中看到过这个设计模式,所以我会从我的角度给你简要概述一下这个设计和它与 Unity 的关系。尽管你将看到更多的是设计的一个模拟版本,但模仿它对于 Unity 项目来说还是很有好处的。这关乎于理解你如何将你的代码分散到不同的依赖中,驱动每个类而不是将一堆属性聚集成一个脚本。这样做的好处是,它为你的代码引入了灵活性,而不会对其他属性产生连锁反应。
下面的图示显示了依赖注入如何简单地为一个对象提供另一个对象:

图 1.6 – 依赖注入设计模式
该设计模式的项目中实现了其元素,并被称为依赖注入模式,但在实践中,我们也在下一节中涵盖了 SOLID 原则中的依赖反转原则。
这是一份所有可以让你在众多 Unity 程序员中脱颖而出的众多设计模式的样本。根据我在大学期间的经历以及通过 Unity 程序员角色的进步,这些模式并没有得到足够的运用。然而,如果你理解了它们(正如你将在这些项目中看到的那样),记录下来以免忘记,并且每次你开始或加入一个项目时,都要考虑一下与你选择或分配的角色相匹配的模式。直接跳入并开始编码是非常诱人的,但这也可能是你遇到死胡同或过度庞大类的地方。
让我们继续前进,看看编码的 SOLID 原则。我简要地提到了它们几次——把它们视为一个指导方针,了解什么是一个好的程序员。
SOLID 原则
当你作为一个面向对象编程(OOP)实践者,在 Unity 中规划和编码你的项目——基于包含数据的对象进行编程——模式是使事物统一、节省时间的好方法,并且希望与使用相同模式的程序员建立联系。
然而,你并不总是为你的所有项目都有设计模式,并且尝试强制实施不切实际的计划可能并不实用。如果我们把设计模式放在一边,还有更深层次的编程方法——SOLID 原则。这些原则是指导方针,提醒面向对象编程程序员在编码项目时应该考虑什么。它们概述了你应该和不应该用你的代码做什么。是的,你可以忽略 SOLID 原则,甚至忽略设计模式,但困难将会发生,并增加你编码到死胡同和更改一行代码时产生多个错误的风险。你的同事会挠头,不知道你的代码在做什么,以及可能减慢你的系统的低效类和方法——列表还在继续。不遵循计划并急于完成任务是非常容易的。最终,这会困扰你,你必须接受你需要一个计划,你需要遵守规则,尤其是如果你的项目扩展了,即使只是你自己编码项目也是如此。
五个 SOLID 原则如下:
-
单一职责原则
-
开放/封闭原则
-
李斯克夫替换原则
-
接口隔离原则
-
依赖倒置原则
让我们更详细地看看每一个,从 SOLID 中的"S"——单一职责原则开始。
单一职责原则
一个类应该只有一个目的;对类的更改应该仅针对类的规格。
这意味着我们应该保持类简单,不要让我们的类承担多个角色。如果你有一个处理子弹的类,不要让任何额外的功能落入其中;将它们留给另一个类。程序员经常遇到的一个常见问题是创建一个不断增长和变化的类。这最终会引发问题,通常会导致重构代码,尤其是当你想向你的类添加特定内容时,这可能会影响与之相关的其他属性。
现在我们转向 SOLID 中的"O"——开放/封闭原则。
开放/封闭原则
脚本应该易于扩展但不易于修改。
创建一个类,使其能够支持对其应用额外的工作,而无需不断回访和修改你的原始类。例如,如果我有一艘能够发射子弹的宇宙飞船,并且我想添加不同类型的武器,我不想在原始脚本中添加任何更多内容。如果我想添加 50 种武器,我的原始脚本会简单地不断增长,变得难以控制。理想情况下,这个武器脚本应该能够接收一个扩展,可以替换发射的武器,即使我有 50 种武器,脚本也不需要改变;它只需从扩展中替换武器。例如,一个接口(docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface)或一个接收武器类型的抽象类(docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/abstract)与这个功能配合得很好。如果鼓励类增长并适应所有这些武器,你将开始违反单一职责原则。
现在我们来谈谈 SOLID 原则中的“L”——里氏替换原则。
里氏替换原则
对象中的内容应该能够用子类型的一个实例替换,而不会改变对象本身的基类。
如果一个类从另一个类继承,那么继承的类应该能够像访问原始基类一样访问原始基类。但这是什么意思呢?所以,从理论上讲,子类应该能够以与父类相同的方式被使用。
这种方法的主要好处是你可以有一个包含可以针对特定对象重写值的方法的类。例如,如果一辆车是工厂制造的,那么它出厂时的默认颜色是白色。即使这些白色汽车是同一款车,也可以喷上不同的颜色;颜色被另一个工厂所覆盖。
默认情况下,基类可以返回一个默认值,而使用相同方法的子类可以对其进行覆盖。如果忽略这个原则,你可能会使用大量的if语句来覆盖每个子类的类方法,这将涉及代码的扩展,从而违反了我们之前提到的两个原则。
现在我们来谈谈 SOLID 原则中的“I”——接口隔离原则。
接口隔离原则
特定的接口优于一个通用的接口。
这个相当简单——在实现接口时,尽量保持它们轻量级,并且可以与其他类和方法互换,而不是有一个巨大的接口,这个接口可能对你继承的具体类来说是多余的。例如,一个接口可以包含健康点数、防御、力量等变量。如果我要将这个应用到角色上,这就有意义了,但如果我也将它应用到苹果上,那就没有这么多了。然而,我仍然必须将它应用到苹果上,因为它是接口。
现在让我们继续到 SOLID 中的“D”——依赖倒置原则。
依赖倒置原则
依赖抽象而非具体实现。
抽象类可以用来覆盖一个类的通用领域,这对于像枪脚本这样的东西来说是非常理想的。你可以有多个不同类型的枪,具有不同的射击功率、子弹数量等等。否则,你将依赖于特定的枪类,这可能导致对该特定枪类的多个变体和类调用。抽象意味着无论有多少枪类,都不会影响为所有变体提供服务的其他脚本。
总结来说,SOLID 原则鼓励你将代码分割成多个部分,并移除不断扩展的类。它鼓励你寻找编写代码的其他方法,而不是在编写新代码时引发连锁反应。
因此,当涉及到一个项目时,如果你忽略了 SOLID 原则和设计模式,你可能会存活下来,但你将会塑造出你下一个项目将是什么样的样子。最终,这会变成一个坏习惯,而且重新训练自己会变得更加困难。说到项目,让我们来看看我们即将制作的《致命波浪》游戏的设计概要!
设计《致命波浪》游戏
这将是我们的原型/演示,一个未来经典侧滚动射击游戏,我们可以升级我们的飞船并摧毁以致命波浪形式向玩家进攻的敌人!我们已经讨论了设计模式和 SOLID 原则,我们将更详细地研究它们,包括 Unity 程序员考试的六个核心目标。我们将通过构建游戏结构,将其分解成框架的各个部分。我们需要了解每个类之间的关系,并知道如何扩展我们的代码,而不会破坏我们已写的内容。
我们将使用星星飞驰的环境来覆盖粒子效果,以及具有多种粒子颜色的船推进器。我们将动画化敌舰,并使用 Unity 自带的 API 来动画化我们的环境场景。清单相当长,但如果我们完成这个项目,我们不仅将准备好应对 Unity 程序员考试,而且我们也将准备好以正确的方法扩展这个项目并承担其他项目;所以,让我们来看看以下制作《致命波浪》的指令集(或者用概要来描述它会更好)。
游戏设计概要
标题:杀手波
类型:横版射击游戏
平台:PC/移动
目标受众:10 岁以上
游戏概念:在太空中,玩家需要在有限的生命值内尽可能多地射击和摧毁敌人,直到关卡结束。敌人有两种形式——波和逃跑。第三级将出现一个大型飞行机器人 Boss,玩家需要将其驱逐。
游戏流程和机制:玩家将操控一个小型飞船,可以使用键盘/手柄光标控制移动,并使用Ctrl或Command键从当前位置发射,可以向右移动。当玩家从左到右穿越到舞台的尽头时,关卡结束。
限制:当游戏未连接到互联网时,玩家有三条生命。玩家将被限制在屏幕边界内。
视觉效果:HUD——游戏得分、游戏生命值、游戏标题、关卡标题、游戏结束标题。
敌波:出现在所有三个级别中。这些敌人将以正弦波模式从屏幕右侧向左侧缓慢移动。敌人可以独立飞行,也可以在同一类型的敌人群体中飞行,留下轨迹。
敌人逃跑:这种敌人将被放置在第三级的虚线区域内。如果处于范围内,它们的行为将是远离玩家。如果失去范围,敌人将保持静止。如果与玩家接触,玩家将失去生命值,敌人将死亡。
敌人 Boss:Boss 出现在第三级,向玩家打招呼,然后迅速从屏幕视图中消失,玩家自动追击。
我们现在已经尝到了游戏将是什么样的。作为一个程序员,我们不仅需要考虑游戏是如何制作的,还需要考虑如何扩展它。我们需要考虑如何构建关卡和如何构建敌人。我们如何做到这一点?我们是否需要考虑在开发过程中不减速的情况下动态改变每个关卡的设计?如果你能想到所有与游戏制作相关的事情以及哪些部分需要分解,你将在开发过程中节省很多时间。接下来,我们将讨论游戏的架构。
杀手波游戏框架
我们现在大致知道了我们要制作的游戏类型。不必过于担心具体细节,因为它不会影响开发阶段。我们的主要关注点将是游戏的框架;我们将在后面的章节中关注外观。
进入游戏开发非常容易,可以边做边想——有时,这本身就是一半的乐趣。但是,当涉及到创建任何应用程序的基本原理时,我们需要知道如何通过坚持特定的开发流程来发挥我们的创造力和逻辑。理想情况下,你需要一个框架。
在我继续假设您知道什么是框架之前,让我们先将其视为一个整体蓝图——一个规划,说明我们所有的代码片段将如何相互交流。框架类似于设计模式——代码的规划已经确定,理想情况下不应该扩展,因为我们正在坚持一个计划。
是的,我们知道一些设计模式,但如果我们代码的整体流程和方向缺乏范围,我们很可能会遇到自己陷入死胡同的问题。有时,开发者认为因为他们已经学习了一个框架,他们就可以用它来构建任何应用程序,并且他们在每个项目中都使用它。
当然,我们不应该这样做。我们对模式了解得越多,当我们扩展整体主计划或游戏框架时,代码的流程就会越容易。
制作这个演示有多种方式,可以使用多种模式和框架。我们将遵循的是我的版本,但如果您有一个更好的版本或您感觉更舒适,请随意使用。只要您理解即将到来的章节中描述的过程,并在过程中使用 Unity 的 API,我会鼓励您这样做;否则,只需跟随我们的示例即可。
那么,既然这样,让我们继续我们的游戏框架。
框架
首先,我们将分解我们为游戏所需的内容。我首先想到的是玩家、玩家做什么以及什么将与我们的玩家交互。我们还知道将有一个敌人列表。最后,游戏可能有多场景,因此我们需要考虑每个单独的资产如何在每个可丢弃的场景中设置。像往常一样,事情需要分解成类,我们需要规划类之间连接的重要性。以下是我如何将游戏设计概述分解成单独的类的。
这些是 类职责:
-
SceneManager将全局告知所有类用户处于哪个场景(例如,标题屏幕、第 1 级、菜单屏幕等)。 -
GameManager与所有游戏对象通信,并与其他管理器通信;它负责游戏的循环过程。 -
ScoreManager在离线时读取和写入分数数据,并更新分数用户界面。 -
PlayerShipBuild接收并设置定制设置到PlayerSpawner。 -
EnemySpawner类似于PlayerSpawner,但它可以管理所有不同类型的敌人。 -
Enemy指的是多个敌人类——例如,如果创建了一个射击敌人,它将进入框架的这个位置。如果敌人移动或行为不同,它也将被放入相同的分配。 -
EnemyBullet以设定速率移动,并在设定时间后或与场景接触后移除自己。 -
PlayerSpawner在屏幕的某个位置启动玩家并保持其层次结构有序。 -
Player发射子弹,接收用户的输入控制,如果与场景、敌人或敌人的子弹接触,则被移除。 -
PlayerBullet以固定的速度移动,移除并损坏敌人,并在设定的时间后或与场景接触后移除自身。 -
ShopPiece处理玩家船升级选择的内 容。 -
SOShopSelection保存商店菜单中每个网格选择中使用的数据类型。 -
SOActorModel保存与其连接的每个类的公共变量。例如,所有移动对象都有一个速度设置;这包括玩家子弹、敌人子弹、敌人飞船等等。 -
IActorTemplate不是一个类,而是一个接口。接口有点像与它连接的任何东西的合同。例如,这个接口希望连接到它的类具有名为Attack()、Die()等的函数。类必须包含这些函数,即使它们是空的。希望你已经知道接口是什么;我们将在本书中频繁使用它们。有关接口的更多信息,请参阅learn.unity.com/tutorial/interfaces。
下面的图表展示了我们刚刚列出的每个类之间的视觉关系。这些图表通常被称为统一建模语言(UML)(www.c-sharpcorner.com/UploadFile/nipuntomar/uml-diagrams-part-1/)。我们本可以使用比下面更详细的图表,但为了尽可能保持简单,我们只需用方框和名称来指代类。
一些同学可能会觉得这个看起来像壳的框架比较复杂,但两边是镜像的,并且分别控制游戏对象的职责。让我们更详细地看看:

图 1.7 – 杀手波特的 UML
每个灰色方框代表前面列表中提到的类;方框之间的线条表示它们对类的依赖关系。例如,PlayerSpawner 类将需要与 GameManager 类耦合,以通知它 Player 类正在发生的事情;Player 类将需要直接向 GameManager 类发送和接收有关生命、敌人击杀数和其他统计数据等信息。如果我们想将我们的分数移动到设备上存储,那么我们可以将其链接到我们的 ScoreManager 类。从这个图表中我们可以得出的主要结论是,如果一条线连接到任一方框,则将在类之间进行通信。
UML 不是考试的重点,但鉴于我们正在为游戏制定计划,在这个阶段应该提到它们。我个人喜欢创建 UML,从某种意义上说;只要理解了游戏的流程,我们就不必担心最终确定每个细节。
因此,现在我们有了关于游戏如何工作的概念,我们将如何将其分解成段,以及段与段之间的关系。下一步是准备我们的 Unity 版本,并开始规划如何将游戏迁移到这个软件上。
设置 Unity
Unity 通常每两周就会推出一个新版本的软件。您可能会预期这会导致在考试中跟上最新版本时出现问题。到目前为止,Unity 并没有每年更新他们的考试,因此考试和我们的项目与 Unity 2020 LTS 相关。Unity 将始终通过未来的发布不断更新和引入新的组件和功能,但基本原理应该保持不变。
本书是为那些至少使用 Unity 两年以上的用户设计的,因此我假设您至少拥有他们的免费账户,并且安装了 Unity 的副本。如果没有,这里提供了他们 2020 LTS 文档中的许可激活指南:docs.unity3d.com/2020.2/Documentation/Manual/LicensesAndActivation.html。
一旦您创建了免费账户,您就可以下载 Unity Hub。这将保存您安装的 Unity 版本和您的项目。
您可以从这里下载并安装 Unity Hub:unity3d.com/get-unity/download。
完成上述操作后,您就可以获取 Unity 2020 LTS 的免费副本。
您可以从他们的存档中下载 Unity 2020 LTS:
-
点击列表中的任意一个 LTS Release 2020 链接。
-
然后从滚动窗口中点击Unity Hub按钮。
-
按照其余说明下载 Unity 2020 LTS。
值得注意的是,在安装过程中,您应该确保已经安装了一个 IDE。如果没有,Unity 建议下载Microsoft Visual Studio Community 2019。所有的脚本都将在这个应用程序中编写。以下截图显示了推荐的 IDE 选择:
![图 1.8 – 从 Unity Hub 中选择 Microsoft Visual Studio Community 2019,如果您计划安装游戏的 Android 版本,请勾选旁边的所有三个复选框]
![img/Figure_1.08_B18381.jpg]
图 1.8 – 从 Unity Hub 中选择 Microsoft Visual Studio Community 2019,如果您计划安装游戏的 Android 版本,请勾选旁边的所有三个复选框。
信息通知
如果您计划在手机、平板电脑等设备上安装任何 Unity 项目,请确保您选择了相关的支持文件。在这本书中,我们将简要介绍 Android 版本。我们的主要重点是运行游戏在 Unity 编辑器中。
一旦您安装了 Unity,您就可以运行 Unity Hub 程序。
通过 Unity Hub 启动我们的项目
到目前为止,您已经安装了 Unity 2020 LTS,注册为 Unity 用户,并且有一个快捷方式来运行 Unity Hub 程序。
Unity Hub 主要用于保存安装的不同版本的 Unity,以及系统上和 Unity 自有云存储软件中的项目列表。
当您运行 Unity Hub 时,请确保您已登录为注册用户,如前一小节所述。
打开 Unity Hub 并登录,如果您还没有的话:
- 以下截图显示了您在 Unity Hub 中登录的位置:
![图 1.9 – 登录您的 Unity 账户]
![img/Figure_1.09_B18381.jpg]
图 1.9 – 登录您的 Unity 账户
- 登录后,转到 Unity Hub 顶部的左侧的 项目 标签(如下截图所示用 1 标记),然后选择 新 旁边的向下箭头(如下截图所示用 2 标记)以选择运行此项目的 Unity 版本,如下所示:
![图 1.10 – 在 Unity HUB 中创建一个新项目]
![img/Figure_1.10_B18381.jpg]
图 1.10 – 在 Unity HUB 中创建一个新项目
- 从下拉菜单中,您应该看到 Unity 2020 LTS 的一个副本,这是我们从前一小节中的存档链接中安装的。选择 Unity 2020 LTS。
在 Unity 编辑器启动之前的最后一个屏幕是两个模板之间的选择:
-
3D:Unity 编辑器以三维视图启动。
-
2D:Unity 编辑器以二维视图启动。
让我们创建我们的 Unity 项目:
-
选择 3D。
-
给您的项目起一个名字。我的是
KillerWave。 -
添加您想要存储 Unity 项目的位置。
-
点击 创建项目:
![图 1.11 – 选择一个带有名称和位置的 3D 项目模板以创建项目。确保您在 Hub 窗口的顶部选择了 Unity 2020 LTS]
![img/Figure_1.11_B18381.jpg]
图 1.11 – 选择一个带有名称和位置的 3D 项目模板以创建项目。确保您在 Hub 窗口的顶部选择了 Unity 2020 LTS
您选择的模板实际上并不重要,因为一旦 Unity 编辑器加载,我们只需按键盘上的 2 或者在 Scene 窗口的顶部点击 2D 按钮即可在 2D 和 3D 之间切换,如下截图所示:
![图 1.12 – Unity 编辑器中的 2D 和 3D 模式]
![img/Figure_1.12_B18381.jpg]
图 1.12 – Unity 编辑器中的 2D 和 3D 模式
点击 创建项目 后,您将看到 Unity 编辑器。
接下来,我们将通过以下步骤移除 Unity 的当前文件夹设置和场景,并用我们自己的替换:
- 在
Scenes文件夹中,按键盘上的删除键。
将出现一个窗口,要求确认删除场景。
-
点击 删除。
-
在 Unity 编辑器顶部,选择 文件 | 另存为...
-
确保您的项目正在保存到您的项目
Assets文件夹中。 -
将场景命名为
testLevel。 -
点击 保存。
现在,让我们深入一点,通过使用 Unity Dashboard 检查我们的账户,以了解更多关于我们项目的信息。
仪表板
使用您的 Unity 账户,您可以访问有关账户和其他服务(如Cloud Build)的更多信息。在可能的考试问题中,您可能会被问及仪表板是什么以及您在哪里可以找到有关项目的详细信息。以下是您如何访问仪表板的方法:
- 点击服务窗口右上角的Dashboard链接:

图 1.13 – Dashboard 链接的位置
- 在Unity Dashboard浏览器中,您将看到一系列与 Analytics(在第十一章,存储数据与音频混音器中讨论了存储/操作在线数据)、云构建(在以下信息框中简要提及)等相关的选项和详细信息:

图 1.14 – 突出显示项目成员的 Unity Dashboard
这是 Unity Dashboard,Unity 在这里提供了多项服务,帮助您的游戏更受观众喜爱。我们可以监控玩家的表现,并查找游戏中的任何问题。不要被整个 Unity 仪表板分散注意力,我们自动加载到我们的项目(Killer Wave)中。在这里,我们可以检查一些有用的事情,例如谁在参与项目,并可以添加/删除成员。
Cloud Build
在线构建您推送的项目,适用于多个平台(例如,Android、iOS、PC 等)。这为您和您的团队成员节省了切换平台、在本地机器上构建以及等待再次开始使用 Unity 项目的麻烦。如果同一团队的开发者都在构建同一构建的不同版本,这可能会效率低下并引发问题。使用 Cloud Build,您将获得一个构建编号,这有助于您跟踪当前版本构建。
哇,我们已经涵盖了这么多内容,但这只是第一章!您已经覆盖了一些作为 Unity 开发者不常见的最重要内容。当我作为开发者开始时,我以为这仅仅是让立方体移动、跳跃、发射其他立方体以及使它们看起来更漂亮。在某种程度上,确实如此,但我们需要确保我们在 Unity 项目中避免缺乏结构,因为没有它,事情可能会迅速崩溃,尤其是在扩展项目时。我们将更深入地探讨本章中提到的所有内容,但现在,让我们先回顾一下我们已经覆盖的内容。
摘要
在本章中,你被介绍了考试的六个核心目标。你可能已经跳到了最后的模拟考试并且取得了好成绩,你可能想要提高你的分数并继续我们正在准备的项目。关于项目,随着项目的进行,我们有一些设计模式的想法可以实施(例如,用于管理脚本的单例模式),这些模式将在游戏框架内构建。我们知道 SOLID 原则是什么,随着我们项目的扩展,我们不应该忘记它们。
在下一章中,我们将开始设置testLevel场景中的相机和灯光。我们还将引入我们的玩家飞船,并将其与一些控制连接起来,以便我们可以移动和射击子弹。第一个敌人将带有自己的波浪攻击模式导入。我们还将研究可脚本化对象是什么以及它们如何为程序员和设计师带来好处。
第二章:第二章:添加和操作对象
在上一章中,我们讨论了官方 Unity 程序员考试的重要性以及它可以为任何寻求确保自己或他人理解 Unity 编程的开发者带来的好处。我们还讨论了成为程序员的一般构建块以及我们游戏的设计概要。
作为在游戏引擎上工作的程序员,你可能会为多个行业工作。在这些行业中,你可能会收到一份技术概要/文档(嗯,你应该会!)用于构建应用程序。在这个项目中,我们正在制作一个游戏,游戏设计概要实际上是制作这个游戏的蓝图。在本章中,我们将根据概要和游戏框架的指导,应用我们的大部分代码、游戏对象、预制体等。我们将在本章中提醒自己概要和游戏框架,并将具体信息转移到我们的代码中。
关于我们的代码,我们将讨论接口和可脚本化对象的重要性,以帮助结构化和统一我们的代码,防止其无必要地膨胀,这一点我们在第一章,“设置和结构化我们的项目”,基于 SOLID 原则中已经讨论过。我们还将熟悉 Unity 编辑器,并了解游戏对象、预制体以及导入三维模型进行动画制作。
在本章中,我们将讨论以下主题:
-
设置我们的 Unity 项目
-
介绍我们的接口(
IActorTemplate) -
介绍我们的
ScriptableObject(SOActorModel) -
设置我们的
Player、PlayerSpawner和PlayerBullet脚本 -
规划和创建我们的敌人
-
设置我们的
EnemySpawner和敌人脚本
下一个部分将概述本章涵盖的考试目标。
本章涵盖的核心考试技能
编程核心交互:
-
实现和配置游戏对象行为和物理。
-
实现和配置输入和控制。
-
实现和配置摄像机视图和移动。
在艺术管道中工作:
-
理解光照并编写与 Unity 光照 API 交互的脚本。
-
理解二维和三维动画,并编写与 Unity 动画 API 交互的脚本。
场景和环境设计编程:
-
确定实现游戏对象实例化、销毁和管理的方法。
-
展示对开发者测试及其对软件开发过程影响的了解,包括 Unity Profiler 和传统的调试和测试技术。
-
认识到结构化脚本的模块化、可读性和可重用性技术。
技术要求
本章的项目内容可以在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_02找到。
您可以下载整个章节的项目文件github.com/PacktPublishing/-Unity-Certified-Programmer-Exam-Guide-Second-Edition。
本章的所有内容都存储在相关的 unitypackage 文件中,包括包含我们在本章中将要执行的所有工作的 Complete 文件夹,所以如果您在任何时候需要一些参考资料或额外指导,请务必查看。
查看以下视频,了解 Code in Action:bit.ly/3yfWyt5。
设置我们的 Unity 项目
如果我们不正确地管理文件,将它们放置在分配的文件夹中,项目可能会很快变得混乱。如果您想以自己的方式组织文件夹,或者在本书中,您决定偏离我的做法,那也是可以的。只是当涉及到查找和组织文件时,尽量意识到您未来的自己或其他在这个项目上工作的人。
如果您还没有打开项目,请创建以下文件夹:
-
Model包含 3D 模型(玩家飞船、敌人、子弹等)。 -
Prefab存储游戏对象的实例(这些是在 Unity 中创建的)。 -
Scene存储我们的第一级场景以及其他级别。 -
Script包含我们所有的代码。 -
Material存储我们的游戏对象材质。 -
Resources存储要加载到游戏中的资源和对象。 -
ScriptableObject是能够存储大量数据的数据容器。小贴士
您应该了解预制件是什么,因为它是使 Unity 迅速易用的主要部分之一。然而,如果您不知道:它通常是存储在实例中的具有其设置和组件的游戏对象。您可以通过从 Hierarchy 窗口中拖动游戏对象到 Project 窗口中,将游戏对象作为预制件存储在您的 Project 窗口中。游戏对象名称之后将生成一个蓝色框图标,如果您在 Project 窗口中选择预制件,其 Inspector 窗口将显示所有存储的值。如果您想了解更多关于预制件的信息,可以查看
docs.unity3d.com/Manual/Prefabs.html上的文档。
以下截图显示了如何创建这些文件夹:
![Figure 2.1 – 在 Unity 编辑器中创建文件夹
![img/Figure_2.01_B18381.jpg]
图 2.1 – 在 Unity 编辑器中创建文件夹
接下来,我们将创建子文件夹;我们需要执行以下操作:
- 在我们的
Prefab文件夹中,创建另外两个文件夹,Enemies和Player:
![Figure 2.2 – 在 Unity 编辑器中创建的文件夹
![img/Figure_2.02_B18381.jpg]
图 2.2 – Unity 编辑器中创建的文件夹
Resources是一个 Unity 识别的特殊文件夹。它将允许我们在游戏运行时加载资源。有关Resources文件夹的更多信息,请查看docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity6.html上的文档。
更多信息
在这里提一下StreamingAssets文件夹。尽管我们在这个项目中没有使用它,但它很好地说明了它与Resources文件夹的相似之处(以及不同之处)。
Resources文件夹导入资源并将它们转换为与目标平台兼容的内部格式。例如,当项目构建时,PNG 文件将被转换和压缩。
StreamingAssets文件夹将保存 PNG 文件,不会对其进行转换或压缩。有关 Streaming Assets 的更多信息,请参阅以下链接:docs.unity3d.com/Manual/StreamingAssets.html。
在技术要求部分提供了本章 GitHub 仓库的下载链接。下载后,双击Chapter2.unitypackage文件,我们将得到一个资产列表,这些资产将被导入到我们的 Unity 项目中:
-
Player_ship.fbx -
enemy_wave.fbx
以下截图显示了我们将要导入到项目中的资产的导入窗口:

图 2.3 – 将资源导入到你的项目中
确保所有资源都被勾选,然后点击窗口右下角的导入按钮。我们现在可以继续到下一节,在项目窗口中组织我们的文件和文件夹。
创建预制件
在本节中,我们将创建三个预制件:玩家、玩家的子弹和敌人。这些预制件将包含我们游戏中的组件、设置和其他属性值,我们可以在整个游戏中实例化它们。
让我们从将我们的player_ship.fbx文件制作成预制件实例开始,操作如下。
有时,在导入任何三维文件时,它可能包含我们可能不需要的额外数据。例如,我们的player_ship模型附带其自身的材质和动画属性。我们不需要这些,所以让我们在将模型完全导入 Unity 项目之前移除这些属性。
要移除player_ship模型,我们需要执行以下操作:
-
在
Assets/Model中,选择player_ship文件。 -
在检查器窗口中,选择材质按钮。
-
确保从下拉列表中将材质创建模式设置为无,然后点击应用按钮。
-
现在,点击材质按钮旁边的动画按钮。
-
取消勾选导入动画复选框,然后点击应用按钮。
-
在动画按钮旁边选择绑定按钮。
-
在动画类型下拉菜单中选择当前值,然后选择无,接着点击应用按钮。
-
这就是
player_ship模型的全部内容。重要信息
在整本书中,每次我们选择一个三维模型时,确保运行相同的流程,因为我们不会需要导入额外的元素,就像我们刚才移除的那些。这意味着我现在希望您重复我们刚刚对
enemy_wave.fbx模型所进行的流程。
让我们继续为我们的游戏准备player_ship模型:
-
从
Assets/Model中点击并拖动player_ship到层次结构窗口。 -
在“玩家飞船”中选中
player_ship -
在所有轴向上与
1不同0
下面的截图显示了检查器窗口中的player_ship值:

图 2.4 – 检查器窗口中的玩家飞船值
- 从
Assets/Prefab/Player文件夹中点击并拖动player_ship。
在创建预制体时,有时您可能会被问及这是原始还是变体:

图 2.5 – 创建预制体对话框
变体预制体将是原始预制体的副本,但也将携带从原始预制体中做出的任何更改。例如,如果原始预制体是一辆有 4 个轮子的车,那么变体也将有相同的。如果原始预制体将其数量从 4 改为 3,变体将复制原始预制体。
注意,层次结构窗口中的player_ship已经变成蓝色,这意味着它已经变成了一个预制体。
- 从层次结构窗口中删除
player_ship。
我们将使用类似的过程来创建我们的enemy_wave预制体,但我们也需要创建它自己的名称标签,因为目前还没有敌人标签...
敌人预制体和自定义标签
在本节中,我们将创建一个enemy_wave预制体以及一个自定义标签。该标签将用于识别和分类所有相关敌人。
要创建一个enemy_wave预制体和自定义名称标签,请按照以下说明操作:
-
将
Assets/Model中的enemy_wave.fbx文件拖入层次结构窗口。 -
在
enemy_wave中选中enemy_wave文件。 -
在所有轴向上与
1不同0:

图 2.6 – 检查器窗口中的敌人波值
现在,让我们通过以下步骤为enemy_wave游戏对象创建一个新的标签:
-
在检查器窗口中选择未标记参数。
-
从标签下拉菜单中选择添加标签...。
-
检查器窗口现在将显示标签和层窗口。
-
点击+以添加一个新的标签,如图下截图所示。
-
在弹出窗口中输入
Enemy,如图下截图所示,然后点击保存按钮:

图 2.7 – 向标签和层列表添加标签
-
返回到
enemy_wave游戏对象以恢复我们的检查器窗口详细信息。 -
再次单击未标记参数。
-
我们现在可以在下拉列表中看到敌人,因此选择它。
-
将
enemy_wave游戏对象从Assets/Prefab/Enemies拖动。 -
从层次结构窗口中删除
enemy_wave
我们现在继续进行第三个预制体创建 – 玩家的子弹。但这次,我们不会导入三维模型 – 我们将在 Unity 编辑器中创建一个,然后在下一节中将其创建为预制体。
创建玩家的子弹预制体
接下来,我们将在 Unity 编辑器中创建玩家子弹的视觉效果。我们将制作一个蓝色球体并为其添加一个环绕光源。让我们先创建一个三维球体游戏对象。
在层次结构窗口中右键单击,从下拉列表中选择3D 对象 | 球体。
在层次结构窗口中仍然选中新创建的Sphere,在检查器窗口中进行以下更改:
-
将游戏对象名称从
Sphere更改为player_bullet。 -
将标签从未标记更改为玩家。标签名称使得在章节的后续部分更容易识别。
-
除了所有轴上的
2之外,0。
以下截图显示了所有三个更改:

图 2.8 – 检查器窗口中的 player_bullet 值
接下来,我们将给player_bullet游戏对象添加一个新的蓝色材质。
创建并应用玩家的子弹材质
在本节中,我们将创建一个简单的无光照材质,由于材质的简单性,它不会占用设备太多性能。要创建基本材质并将其应用到player_bullet对象上,请执行以下操作:
-
在
Assets/Material文件夹中。 -
在
Material文件夹内,创建一个新的文件夹(与我们在设置我们的 Unity 项目部分中所做的方式相同)并命名为Player。这样,任何与玩家相关的材质都可以存储在其中。 -
双击新创建的
Player文件夹,然后在项目窗口中(在窗口右侧的空白区域)再次右键单击,从下拉列表中选择创建 | 材质。 -
将创建一个新的材质文件。将其重命名为
player_bullet。 -
选择
player_bullet材质,并在检查器窗口中,将材质从标准着色器更改为不发光 | 颜色,按照以下截图中的三个步骤操作:

图 2.9 – 创建不发光颜色材质
检查器窗口将移除大部分属性并将材质简化,以便在任何设备上更容易执行。
- 仍然在
0,190,255和255。
我们已经创建并校准了玩家的子弹,现在我们可以通过以下步骤将材质应用到player_bullet预制件上:
-
在以下位置选择
player_bullet预制件:Assets/Prefab/Player。 -
在下拉列表中选择
player_bullet,直到你看到材质,然后选择它。
以下截图显示了player_bullet预制件的网格渲染器组件更新到我们新的无光照材质:

图 2.10 – player_bullet现在具有player_bulletMat材质
在*第四章**,应用艺术、动画和粒子中,我们将回到材料和艺术的一般讨论,如果你对此感兴趣,这将值得关注。我们还将玩转粒子系统,创建一队星星飞掠过玩家的飞船。
我们将添加到玩家子弹的最后一个组件是一个环绕灯光,给子弹一个能量发光的效果。
向玩家的子弹添加灯光
在本节中,我们将向玩家的子弹添加一个灯光组件,以隐藏我们只是在发射球体的印象。它还将介绍 Unity 的点光源,它充当发光球体。
要添加和自定义一个球状灯光到玩家的子弹,我们需要做以下操作:
-
在
Assets/Prefab/Player文件夹中,选择player_bullet预制件,并将其拖入层次结构窗口(如果它还没有在层次结构窗口中)。 -
在组件列表底部的检查器中,点击添加组件按钮,从下拉列表中选择灯光。
player_bullet预制件现在将附加一个灯光组件。我们只需更改三个属性值,使灯光更适合游戏对象。
-
在
player_bullet文件的50中更改以下属性值 -
0,190,255, 和255 -
20
以下截图显示了更新值后的灯光组件:

图 2.11 – 检查器窗口中的灯光组件值
在进入下一节之前,因为我们已经对一个现有的预制件添加了材质和灯光组件,我们需要点击覆盖按钮以确认新的更改。
以下截图显示了player_bullet预制件:

图 2.12 – 更新 player_bullet 预制件
- 最后,点击层次结构中的
player_bullet。
在下一节中,我们将继续通过应用 Unity 自己的物理系统,即刚体组件,来更新我们的三个预制件,以帮助检测碰撞。
添加刚体组件和修复游戏对象
因为这个游戏涉及到与游戏对象的碰撞,我们需要对玩家、玩家的子弹和敌人应用碰撞检测。Unity 提供了一系列不同的形状来围绕游戏对象,使其作为一个不可见的盾牌;我们可以设置我们的代码以响应与盾牌接触。
在我们将碰撞器添加到玩家和敌人游戏对象之前(Sphere游戏对象自动带有碰撞器),我们需要添加一个名为Rigidbody的 Unity 组件。如果一个游戏对象将要与至少一个其他游戏对象发生碰撞,它需要一个Rigidbody组件,该组件可以影响游戏对象的质量、重力、阻力、约束等。如果您想了解更多关于Rigidbody组件的信息,请查看docs.unity3d.com/Manual/class-Rigidbody.html上的文档。
Rigidbody 联结
Unity 除了碰撞器之外还有其他物理类型。关节也需要Rigidbody系统,并且它们有不同的形式,如铰链、弹簧等。
这些关节将在一个固定点进行模拟;例如,铰链****关节非常适合使门在门铰链的旋转点来回摆动。
如果您想了解更多关于关节的信息,请查看docs.unity3d.com/Manual/Joints.html上的文档。
让我们一次性添加player_ship和player_bullet预制件:
-
在项目窗口中,导航到Prefab | Player。
-
按Ctrl(在 Mac 上为command)并点击
player_ship和player_bullet文件。 -
在检查器窗口中,点击添加组件按钮。
-
在下拉菜单中,输入
Rigidbody。 -
选择Rigidbody(不是Rigidbody 2D)。
-
Rigidbody组件现在已经被分配给了我们的两个游戏对象。
-
在检查器窗口中仍然选择两个游戏对象,在Rigidbody下,确保重力复选框没有被勾选。如果勾选了,我们的游戏对象在游戏进行时会开始沉入场景。
现在,我们可以将碰撞器添加到我们的player_ship和enemy_wave游戏对象中(我们的player_bullet已经有一个SphereCollider)。我们将为我们的游戏对象添加一个SphereCollider,因为它相对于性能成本来说是最便宜的碰撞器:
-
将
player_ship预制件从Assets/Prefab/Player拖动到层次结构窗口中。 -
在下拉菜单中的
Sphere Collider下仍然选择player_ship。 -
一旦你看到
player_ship游戏对象。
你会注意到在player_ship的碰撞器中有一个绿色的线框围绕在player_ship周围,这将用来检测击中。它可能对于击中框的用途来说太大,所以让我们减小其大小。
- 在检查器窗口中仍然选择
player_ship预制件,在0.3处,如图所示:

图 2.13 – 添加到player_ship预制体的触发球体碰撞器
-
此外,当我们仍然选择
player_ship预制体时,检查player_ship预制体中是否有另一个碰撞器,而不会引起任何形式的潜在物理碰撞。 -
在Inspector窗口的右上角点击Override,然后点击Apply All以更新我们对预制体的Rigidbody和SphereCollider组件所做的修改。
-
我们现在可以在Hierarchy窗口中选择
player_ship预制体,然后在键盘上按Delete键,因为我们不再需要在Scene中使用它。
我们现在需要将同样的方法应用到player_bullet上:
-
将
player_bullet预制体从Assets /Prefab/Player拖放到Hierarchy窗口中。 -
在Inspector窗口的SphereCollider组件中勾选Is Trigger框,并调整Radius。
-
点击
player_bullet更改,并从Hierarchy窗口中删除player_bullet预制体。
我们需要更新的最后一个游戏对象是enemy_wave预制体。我们已经用player_ship和player_bullet预制体覆盖了步骤,所以完全重复指令并不理想;然而,我们需要做以下事情:
-
简而言之,我想让你将
enemy_wave预制体从Project窗口中Assets/Prefab/Enemies的位置拖放到Hierarchy窗口中.. -
在Inspector窗口中添加一个
enemy_wave预制体。 -
调整
enemy_wave预制体的正确比例,就像我们调整player_ship一样。 -
enemy_wave预制体不需要Rigidbody组件,因为它将与自身包含一个组件的相关游戏对象发生碰撞。 -
最后,从Hierarchy窗口中删除
enemy_wave预制体。
使用以下截图作为前面简短说明的参考,如果你卡住了,请使用本节中讨论的先前步骤:

图 2.14 – 添加并缩放到enemy_wave预制体的触发碰撞器
希望这对你来说进展顺利。如果你在任何地方卡住了,请参考包含所有完成文件的github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/blob/main/Chapter_02/ProjectFIles/Chapter-02-Complete.unitypackage文件夹,检查它们并进行比较。
在继续之前,请注意,如果一个游戏对象是粉红色的,例如我们之前截图中的enemy_wave对象,这仅仅意味着它没有附加材质。在其他情况下,这也可能意味着附加到材质上的着色器有问题。
我们可以通过以下方式修复这个粉红色问题:
-
在
Assets/Prefab/Enemies。 -
将 enemy_wave 拖放到层次窗口中。展开层次窗口中
enemy_wave旁边的下拉菜单。 -
选择第一个游戏对象,标题为
enemy_wave_core。 -
在 检查器 窗口中,选择 Mesh Renderer 组件中 元素 0 参数旁边的小 远程 圆圈(在下述截图中用 1 表示),然后从下拉列表中选择 默认材质(在下述截图中用 2 表示),如图所示:

图 2.15 – 将默认材质添加到 enemy_wave_core 游戏对象
- 对于其兄弟游戏对象
enemy_wave_ring,遵循相同的步骤。
enemy_wave 对象现在将应用默认材质。如果对预制体进行了任何更改,请确保点击 覆盖,应用全部。
属性
如果一个游戏对象需要像 Rigidbody 这样的组件,我们可以在类名上方放置一个实际上是对脚本的一个提醒,表明游戏对象需要它:
[需要组件(typeof(Rigidbody))]
如果游戏对象没有组件,脚本将创建一个,如果我们尝试移除 Rigidbody 组件,我们将在 Unity 编辑器中收到一条消息,表明这是一个必需的组件。
这段代码并不是必需的,更多的是一种在组件中的一般良好实践。
如果你想要了解更多关于 RequireComponent 属性的信息,请查看 docs.unity3d.com/ScriptReference/RequireComponent.html 的文档。
因此,现在我们已经将碰撞体和 Rigidbody 组件应用到我们的游戏对象上。这使我们能够在碰撞体相互接触时创建反应。
由于我们开始构建我们的项目,让我们快速讨论保存我们的场景、项目等。
保存和发布我们的工作
容易陷入我们的项目,但作为简短的提醒,尽可能频繁地保存你的工作。这样,如果发生任何不好的事情,你总是可以回滚。
由于我们已经从前一章创建了并保存了 testLevel 场景,我们还可以将此场景添加到 构建设置 窗口中。这样做的原因是让 Unity 知道我们想在项目中包含哪些场景。在将游戏打包为部署的构建时,这也是一个要求。
要将我们的场景添加到 构建设置,请执行以下操作:
-
在 Unity 编辑器顶部,点击 文件 | 构建设置。将出现 构建设置 窗口。
-
点击
testLevel场景。 -
以下截图显示了
testLevel场景。当我们稍后添加更多场景时,每个场景都将被编号:

图 2.16 – 将测试级别场景添加到构建场景列表中
-
关闭 Build Settings 窗口。我们将在下一章添加更多场景时回到这里。
-
通过点击 文件 | 保存项目 来保存项目是一个好习惯。
现在我们继续在 Unity 编辑器中设置场景相机。
Unity 编辑器布局
对于我们的横向卷轴射击游戏 Killer Wave,我们需要控制相机来显示场景的宽高比和可见深度,并确保我们展示了正确数量的游戏环境。
让我们开始并决定我们游戏的屏幕比例。我们将创建自己的分辨率,这在大多数平台上将是相当常见的。
要将 Game 窗口的屏幕比例更改为自定义比例,请执行以下操作:
-
在 Game 窗口选项卡下点击当前宽高比并选择 + 符号。
-
输入以下截图所示的自定义宽高比值。
-
点击我们刚刚创建的
1080分辨率:

图 2.17 – 设置自定义游戏窗口分辨率
了解我们需要使我们的游戏艺术作品支持尽可能多的屏幕比例(或者给它扩展到)的需求是很好的,尤其是如果我们想为平板电脑或手机等便携式设备制作游戏的话。这是因为几乎每个主要品牌的手机和平板电脑都有不同的比例尺寸,我们不希望开始挤压和压缩我们的内容,因为它看起来不会正确。也有可能我们的小型移动游戏会取得成功,并且以后可以被移植到游戏机或 PC 上。如果是这样的话,我们需要让游戏屏幕也支持这些比例。从所有这些中我们可以得出的主要观点是我们正在针对我们的游戏覆盖所有可能的常见屏幕比例。我们可以覆盖的平台(游戏机、便携式设备等)越多,灵活的屏幕比例,就越容易将我们的游戏扩展到那些设备上,而无需额外的工作。我们在第八章,“添加自定义字体和 UI”,和第九章,“创建 2D 商店界面和游戏内 HUD”,中解释了更多关于屏幕尺寸比例的内容,其中我们讨论了 UI 显示设置。此外,在第十三章,“效果、测试、性能和替代控制”,中我们将解释如何在一个原始图像组件上显示我们的游戏屏幕。
在我们继续我们的项目之前,确认我们对 Unity 自身 UI 布局的了解可能是一个好时机。以下截图显示了 Unity 编辑器,我在其中勾勒并标注了相关的窗口:

图 2.18 – Unity 编辑器窗口布局
通常,Unity 编辑器窗口由五个主要窗口组成:
-
场景:这是我们二维/三维的工作空间。
-
游戏:这是最终用户将看到的窗口。默认情况下,游戏选项卡与场景窗口共享相同的空间。
-
层次结构:我们场景中的所有游戏对象都将列在这里。
-
检查器:当选择一个对象时,其信息将在这里显示。
-
项目:这是我们 Unity 项目文件夹。将其视为我们可以用于游戏的文件和文件夹的结构。
小贴士
要单独拖动每个窗口,左键点击并拖动标签名称,然后它会自动定位到不同的位置。
我的游戏窗口设置为1080,因为我没有第二个屏幕的奢侈,所以我点击了它的名称标签(游戏)并将其拉到右下角。窗口很小,但正如您在游戏窗口顶部所看到的,缩放设置为 1x,这意味着我有一个完整的画面;没有任何东西被隐藏或从视野中裁剪掉。
要检查我们是否具有主摄像机的0。我们还可以按照以下方式重置变换选项:
- 在层次结构窗口中选择主摄像机后,在检查器窗口中,点击变换面板右上角的三点,如图下截图所示:

图 2.19 – 变换设置齿轮位置
- 当下拉菜单出现时,点击重置。
继续设置我们的主摄像机,让我们通过更改其背景设置来从场景/游戏窗口中移除景观背景:
-
在层次结构窗口中点击主摄像机。
-
在检查器窗口中,我们有摄像机组件,其属性名为清除标志。点击天空盒值。
-
将出现一个下拉菜单。点击纯色,如图下截图所示:

图 2.20 – 将背景更改为纯色
-
我们现在将看到一个蓝色背景,这会减少干扰。
-
如果您不喜欢蓝色,您可以将它更改为
0,0,0和255中的任何颜色,如图下截图所示:

图 2.21 – 设置背景颜色值
太好了,现在让我们继续为主摄像机编写这些属性。
通过脚本更新摄像机属性
我们现在已经在我们的场景中设置了主摄像机的行为。接下来,我们需要将这段代码编写到一个脚本中,这样每当场景被加载时,Unity 都会读取脚本并理解主摄像机应该如何设置。
再次观察我们的框架,让我们看看摄像机脚本应该放置在哪里:

图 2.22 – 杀手波 UML
如您在图中所见,图中没有提及相机,那么我们应该编写一个脚本来支持这一点吗?可以说,基于相机编写脚本的唯一原因可能是如果相机具有复杂的目的,并且包含多个属性和功能。然而,在我们的游戏中,相机是在游戏开始时放置的。稍后,在第三级,相机将使用简单的组件脚本从左向右移动,但它不包含任何其他复杂性。因此,使用GameManager会更理想,因为它只扮演着一个小角色。如果游戏变得更大,相机承担了更多角色,那么这可能就为相机拥有自己的类提供了理由。其他人可能会根据个人喜好提出不同意见,但我们将采取这种方法。
让我们创建GameManager脚本,如下所示:
- 以创建文件夹的方式创建脚本。在项目窗口的空白区域右键单击,将出现一个下拉菜单。点击创建 | C# 脚本,如下所示:

图 2.23– 在 Unity 编辑器中创建 C#脚本
-
脚本以标题
NewBehaviourScript出现。我们不想这样称呼它,所以(以驼峰命名法)输入GameManager。什么是驼峰命名法?
驼峰命名法是一种避免单词之间空格的方法。这在编程中相当常见,因为出于各种原因,通常不欢迎空格。每个新单词都以大写字母开头,所以在这种情况下,
GameManager中的 M 就是驼峰的顶部。然而,变量通常以小写字母开头,您很快就会看到。
我们现在有了我们的GameManager脚本。注意 Unity 如何试图提供帮助,将图标更改为银色齿轮,因为我们正在执行的是 Unity 中认可的方法:

图 2.24 – GameManager 图标
就像我们将三维模型放入GameManager的Script文件夹中时做的那样。
很好。现在,在我们打开脚本进行编码之前,我们需要将其附加到场景中的游戏对象上,这样当场景运行时,附加到游戏对象上的脚本也会运行。
要创建我们的GameManager游戏对象,我们需要执行以下操作:
-
在层次结构窗口的空白区域右键单击。
-
从下拉菜单中选择创建空对象。
-
右键单击新创建的游戏对象,从下拉菜单中选择重命名。
-
将此游戏对象重命名为
GameManager。 -
最后,仍然选择
GameManager游戏对象,点击最右侧的检查器窗口中的添加组件按钮。 -
从下拉菜单中输入
GameManager,直到您看到GameManager脚本并选择它。小贴士
每次我们创建一个空的游戏对象时,我们必须确保将其所有变换属性值重置为默认值,除非我们明确更改它们。
要重置游戏对象的Transform值,请确保我们正在重置的游戏对象已被选中。点击Inspector窗口右上角的金属齿轮,然后选择Reset。
双击GameManager脚本以在您的 IDE(Visual Studio 或您使用的任何 IDE)中打开它,然后按照以下步骤操作:
-
在
GameManager脚本内部,我们将面临将UnityEngine库导入我们的脚本以向 Unity 自身组件添加额外功能的情况:using UnityEngine; public class GameManager : MonoBehaviour {
在前面的代码中,我们还有我们脚本的名称以及MonoBehaviour被继承,以向我们的脚本添加更多功能。MonoBehaviour也是必需的,如果附加到该脚本的游戏对象需要在 Unity 编辑器中使用。
让我们开始在我们的GameManager脚本中添加一些自己的代码。
-
创建一个空的方法,
CameraSetup,然后在Start函数中运行此方法:void Start() { CameraSetup(); } void CameraSetup() { } -
在
CameraSetup方法内部,添加对相机的引用,并将相机的位置和角度设置为除其z轴外的零。我们将Z设置为-300,这将使相机后退,并确保所有游戏对象都在镜头中:GameObject gameCamera = GameObject.FindGameObjectWithTag("MainCamera"); //Camera Transform gameCamera.transform.position = new Vector3(0,0,-300); gameCamera.transform.eulerAngles = new Vector3(0,0,0); -
接下来,我们将在
CameraSetup方法中更改相机属性://Camera Properties gameCamera.GetComponent<Camera>().clearFlags = CameraClearFlags.SolidColor; gameCamera.GetComponent<Camera>().backgroundColor = new Color32(0,0,0,255); }
这将执行以下操作:
-
移除天空背景,并用实色替换
-
将实色从默认的蓝色更改为黑色
- 最后,保存脚本。
现在,您应该有类似以下的内容:

图 2.25– GameManager 脚本的当前代码布局
提示
如果您想更改与相机相关的其他设置,您可以在docs.unity3d.com/ScriptReference/Camera.html了解更多信息。
在编辑器窗口的右上中部按下Play按钮,或使用快捷键Ctrl + P(在 Mac 上为Command + P)。以下截图显示了Play按钮的位置:

图 2.26 – 播放、暂停和步骤按钮的位置
在场景处于播放模式时,我们可以通过以下方式检查Main Camera游戏对象的属性:
- 在Hierarchy窗口中,选择Main Camera。
观察下一张截图中的Inspector窗口,以查看我们的脚本所做的以下更改。
-
在Inspector窗口的Transform组件中,我们可以看到Position和Rotation属性被设置为与我们在脚本中设置相同的值(以下截图中的1所示)。
-
在Inspector窗口的Camera组件中,我们可以看到Clear Flags和Background值也被设置为与我们在脚本中设置的相同值(以下截图中的2i和2ii所示)。
以下截图显示了在播放模式下Main Camera组件属性更新的情况:

图 2.27 – 主相机值随着我们的脚本变化
现在,希望我们的属性应该与我们所编写的脚本相同(没有错误)。如果不是,你可能会在控制台窗口中看到一个错误消息。如果有错误,它可能会告诉你错误所在的行。你还可以双击错误,它会带你到错误所在的行。
为了确保一切正常,请在编辑器中更改相机的位置和旋转,然后按播放按钮。相机的属性现在应该设置为脚本中的位置和旋转属性。
在这一点上,当编辑器仍在播放时,我们还可以创建一个相机的预制体:
- 点击并拖动相机从层次结构窗口到项目窗口,我们将生成一个带有相机名称或空图标蓝色的立方体。根据我们图标的尺寸,可以通过以下截图所示的滑块来调整图标的大小:


图 2.28 – 项目窗口右下角的滑块可以放大和缩小缩略图
- 将这个相机预制体移动到
Prefab文件夹。
你可能会想,为什么我们一开始不直接创建一个相机的预制体,而不是在代码中调整其属性设置呢?然而,这里有两个关键点很重要:首先,我们正在为可能涵盖此类属性的考试做准备;其次,你现在知道如何通过代码动态更改这些设置。
小贴士
使用 Unity 组件进行脚本编写的好处是,我们有时可以获得比编辑器中显示的更多功能。例如,Camera组件有一个layerCullDistances属性,只能通过脚本访问。这可以提供诸如跳过渲染远距离较小游戏对象以增加游戏性能等功能。
要了解更多关于layerCullDistances的信息,请查看docs.unity3d.com/ScriptReference/Camera-layerCullDistances.html的文档。
这部分内容到此结束。到目前为止,我们已经涵盖了以下内容:
-
设置游戏相机的比例
-
设置我们的 Unity 编辑器中的单独窗口
-
在 Unity 编辑器中更改我们的相机组件属性
-
重复我们在
GameManager脚本中对相机所做的更改 -
将我们的
GameManager脚本作为游戏对象添加到场景中
作为程序员,能够理解和更改 Unity 编辑器中的设置(同时也能在代码中做到这一点)的重要性可以扩展到编辑器中的其他组件。这就是我们接下来要做的,重点关注方向光。
设置我们的灯光
作为默认设置,每个场景都附带一个方向光,目前这是我们开始所需的所有;理想情况下,我们希望场景得到良好的照明。
在场景中已经存在作为默认灯光的方向光,在 50,-30 和 0 中选择它。
当我们将玩家飞船放入场景中时,这将很好地照亮它,如下面的截图所示:
![Figure 2.30 – 照亮的玩家飞船
![img/Figure_2.30_B18381.jpg]
图 2.29 – 照亮的玩家飞船
不同的灯光
Unity 提供了三种不同类型的实时灯光。除了我们提到的 方向 光之外,它还提供了一个 点 光,这是一种 360° 的发光,我们将在 第四章 应用艺术、动画和粒子 中介绍。第三种灯光类型是聚光灯,或如 Unity 所称的 spot。spot 也可以应用遮罩,因此它可以投射称为 cookies 的图像。
想了解更多关于三种灯光类型的信息,请查看 docs.unity3d.com/Manual/Lighting.html。
我们现在可以通过将它们添加到 GameManager 脚本来确保这些设置保持不变。我们还可以更改灯光的颜色。
通过脚本更新我们的灯光属性
在 GameManager 中,我们将设置 Transform Rotation 值,并将颜色色调从浅黄色更改为冷蓝色:
-
打开
GameManager脚本并输入以下方法:void LightSetup() { GameObject dirLight = GameObject.Find("Directional Light"); dirLight.transform.eulerAngles = new Vector3(50,-30,0); dirLight.GetComponent<Light>().color = new Color32(152,204,255,255); } -
在
Start函数的作用域内添加LightSetup();。 -
保存脚本。
LightSetup 方法做了三件事:
-
它从场景中获取灯光并将其存储为引用。
-
它使用
EulerAngles设置灯光的旋转。 -
最后,它改变了灯光的颜色。
EulerAngles
eulerAngles允许我们给出Vector3坐标而不是Quaternion值。eulerAngles使得旋转操作更加简单。有关eulerAngles的更多信息,请参阅docs.unity3d.com/ScriptReference/Transform-eulerAngles.html。
这就是我们需要的关于灯光的所有操作。与相机一样,我们可以通过脚本访问灯光并更改其属性。
我们通过在 Unity 编辑器和 GameManager 脚本中更改其设置已经熟悉了我们的灯光。接下来,我们将为大多数游戏对象设置我们的接口。
介绍我们的界面 – IActorTemplate
IActorTemplate 接口是我们用来提示伤害控制、死亡和可脚本化对象资产的。使用此类接口的原因是它将继承它的类之间的通用用途联系起来。
总共有六个类将使用 IActorTemplate 接口,如下所示:
-
Player -
PlayerBullet -
PlayerSpawner -
Enemy -
EnemyBullet -
EnemySpawner
下图显示了 IActorTemplate 接口以及我们游戏框架的部分概述:

图 2.30 – IActorTemplate UML
让我们创建我们的接口并在过程中解释其内容:
-
在
Assets/Scripts文件夹中创建一个名为IActorTemplate的脚本。 -
打开脚本并输入以下代码:
public interface IActorTemplate { int SendDamage(); void TakeDamage(int incomingDamage); void Die(); void ActorStats(SOActorModel actorModel); } -
确保保存脚本。
我们刚刚输入的代码看起来像我们声明了一个类,但它的行为本质上不同。我们不是使用class关键字,而是输入interface后跟接口名称IActorTemplate。虽然不是必须以I开头命名任何接口,但这使得脚本易于识别。
在interface中,我们创建了一个方法列表,这些方法就像是对实现它们的任何类的合同。例如,我们将在本章后面创建的Player脚本继承自IActorTemplate接口。Player脚本必须声明来自IActorTemplate的函数名,否则Player脚本将抛出错误。
在interface的作用域内,我们声明方法而不需要访问器(这意味着每个方法的开头不需要private或public)。方法也不需要任何内容(也就是说,它们是空的)。
更多关于接口的信息,请参阅learn.unity.com/tutorial/interfaces。
我们interface中的最后一个方法是ActorStats,它接受一个SOActorModel类型。SOActorModel是一个可脚本化的对象,我们将在下一节中解释和创建。
介绍我们的可脚本化对象 – SOActorModel
在本节中,我们将介绍可脚本化对象及其优点。与我们的interface一样,可脚本化对象覆盖了相同的六个类。这样做的原因是,我们的interface使用了SOActorModel,因此与其他变量建立了关联。
也要提醒自己游戏设计文档(GDD)以及它是如何融入我们游戏创建概述中的。
我们的游戏有三个系列的游戏对象,它们将具有相似属性:EnemyWave、EnemyFlee和Player。这些属性将包括健康、速度、得分值等。这些对象之间的区别,如游戏设计简报所述,在于它们的行为以及它们在我们游戏中的实例化方式。
Player将在每个级别实例化,EnemyWave将从EnemySpawner生成,而EnemyFlee将被放置在第三级的特定区域。
所述的所有游戏对象都将与SOActorModel对象相关联。
以下图表也是我们游戏框架的部分视图,显示了可脚本化对象及其继承的六个类:

图 2.31 – SOActorModel UML
与前面提到的 interface 脚本类似,脚本化对象的名字以 SO 开头,这并不是命名脚本的常规方式,但它更容易被识别为 ScriptableObject。
此脚本化对象的目的在于为每个分配给它的游戏对象保存通用值。例如,所有游戏对象都有一个名称,因此在我们的 SOActorModel 中有一个名为 actorName 的 string。这个 actorName 将被用来命名敌人、生成器或子弹的类型。
让我们创建一个脚本化对象,如下所示:
-
在
Assets/Scripts文件夹中,文件名为SOActorModel。 -
打开脚本并输入以下代码:
using UnityEngine; [CreateAssetMenu(fileName = "Create Actor", menuName = "Create Actor")] public class SOActorModel : ScriptableObject { public string actorName; public AttackType attackType; public enum AttackType { wave, player, flee, bullet } public string description; public int health; public int speed; public int hitPower; public GameObject actor; public GameObject actorsBullets; } -
保存脚本。
在 SOActorModel 中,我们将命名 Player 脚本中的大多数,如果不是所有这些变量。类似于 interface 与类签订合同的方式,SOActorModel 也做了同样的事情,因为它正在被继承,但它不像 interface 那样严格,如果脚本化对象的内容没有被应用,它会抛出一个错误。
以下是我们刚刚输入的 SOActorModel 代码的概述。
我们将可脚本化对象命名为 SOActorModel,作为一个通用术语,试图涵盖尽可能多的可能使用脚本化对象的游戏对象。这种工作方式也支持我们在第一章中提到的 SOLID 原则,它鼓励我们尝试保持代码简洁高效。
我们将为本脚本涵盖的主要类别如下:
-
SOActorModel脚本使用using UnityEngine;不需要其他库。 -
CreateAssetMenu属性在 Unity 编辑器中右键单击并选择 创建 时,在 项目 窗口的下拉列表中创建一个额外的选择,如下面的截图所示:
![Figure 2.33 – 在 Unity 编辑器中创建 Actor]
![img/Figure_2.33_B18381.jpg]
图 2.32 – 在 Unity 编辑器中创建 Actor
-
使用
MonoBehaviour而不是ScriptableObject,因为这是创建资产时的一个要求。 -
变量:最后,这些是将被发送到我们选定类中的变量。
在接下来的几节中,我们将从脚本化对象脚本创建资产,以赋予我们的脚本不同的值。
创建 PlayerSpawner 脚本化对象资产
在我们创建的 SOActorModel ScriptableObject 之后,我们现在可以创建一个资产,它将作为一个模板,不仅可供程序员使用,还可以供想要调整游戏属性/设置但不需要了解如何编码的设计师使用。
要创建 Actor Model 资产,请按照以下步骤操作:
-
在 Unity 编辑器中,回到 项目 窗口,右键单击并选择 创建 | 创建 Actor。
-
将新创建的资产文件重命名为
Player_Default并将其存储在Assets/Resources文件夹中。 -
点击新资产,在 检查器 窗口中,您将看到资产的内容。
以下截图显示了 Actor Model 资产的字段,其中我已输入自己的值:
![图 2.34 – 玩家值
![图 2.34 – 图 2.34_B18381.jpg]
图 2.33 – 玩家值
让我们逐一分析添加到我们新创建的资产中的每个值:
-
Player)。 -
船型:选择这个游戏对象属于哪个类别。
-
描述:设计师/内部备注,不影响游戏但可能有所帮助。
-
健康值:玩家在死亡之前可以承受多少次打击。
-
速度:玩家的移动速度。
-
打击力:确定玩家与敌人相撞时会造成多少伤害。
-
player_ship预制体在这里(Assets/Prefab/Player) -
player_bullet预制体在这里(Assets/Prefab /Player/)。
我们将在本章后面构建此资产后将其添加到PlayerSpawner脚本中。让我们继续下一个可脚本对象资产。
创建一个EnemySpawner ScriptableObject 资产
在本节中,我们将使我们的敌人资产附加到EnemySpawner,以便在章节的后面部分使用。为了保持我们的工作新鲜完整,让我们继续进行,然后再转到EnemySpawner脚本。
要创建一个敌人资产,请按照以下说明操作:
-
在编辑器中,在项目窗口中,右键单击并选择创建 | 创建演员。
-
将新文件重命名为它所附加的内容(
BasicWave Enemy)并将文件存储在Assets/ScriptableObject位置。 -
点击新脚本,我们的检查器窗口将显示脚本的内容。
以下截图显示了完成后的BasicWave Enemy资产将看起来像什么:
![图 2.35 – 基本波敌人值
![图 2.35 – 图 2.35_B18381.jpg]
图 2.34 – 基本波敌人值
让我们简要地过一下我们敌人的每个值:
-
enemy_wave。 -
Wave。这解释了敌人的类型以及它是如何攻击玩家的。 -
通常在组中。如前所述,这更多的是一个指南而不是规则,用于注释任何内容。 -
1,这意味着它需要 1 次打击才能死亡。 -
-50,因为我们的敌人是从右向左移动的,所以我们给它一个负数。 -
1,这意味着如果这个敌人与玩家相撞,它将造成 1 点伤害。 -
enemy_wave预制体在这里(Assets/Prefab/Enemies)。 -
演员子弹:这个敌人不发射子弹。
希望你能看到可脚本对象有多有用。想象一下,如果我们继续开发这个游戏,有50个敌人,我们只需要创建一个资产并对其进行自定义。
我们将在下一节继续本章的最后一个可脚本对象资产。
创建 PlayerBullet ScriptableObject 资产
在本节中,我们将为玩家开火时创建一个玩家子弹的资产。与最后两个部分一样,创建一个资产,命名为PlayerBullet,并将其存储在其他资产相同的文件夹中。
以下截图显示了PlayerBullet资产的最后结果:
![图 2.36 – 玩家子弹值
![图 2.36 – 图 2.36_B18381.jpg]
图 2.35 – 玩家子弹的值
让我们简要地过一下每个变量的值:
-
player_bullet。 -
船型:子弹。
-
描述:在此处输入有关资产的任何详细信息是可选的。
-
1。 -
700。 -
1发送 1 点伤害值。 -
player_bullet预制体在这里(Assets/Prefab/Player)。 -
演员子弹:无(游戏对象)。
在后续章节中,当我们为游戏构建商店时,我们将能够为玩家的飞船购买增强功能。其中一个增强功能将与我们刚刚制作的类似,但演员名称将不同,伤害力将更高。
现在,我们可以继续到下一部分,创建玩家的脚本并将这些资产附加到它们上。
设置我们的 Player、PlayerSpawner 和 PlayerBullet 脚本
在接下来的几个部分中,我们将创建三个脚本,这些脚本将涵盖以下内容:创建玩家、玩家的控制以及玩家的子弹。
我们将创建并包含的脚本如下:
-
PlayerSpawner:创建和校准玩家 -
Player:玩家控制和一般功能 -
PlayerBullet:子弹移动和一般功能 -
IActorTemplate:分配给特定对象的预期规则模板(已创建) -
SOActorModel:一组可以被非程序员修改的值(已创建)
我们将对所有这些脚本进行彻底的讲解,并分解它们各自的目的,以及它们如何相互依赖和通信。我们将从PlayerSpawner开始,它将创建玩家的飞船并分配其值。
设置我们的 PlayerSpawner 脚本
PlayerSpawner脚本的目的是将它附加到一个游戏对象上,从而使玩家在游戏中的该位置出现。当PlayerSpawner脚本被创建时,它还将设置玩家的值。例如,如果我们的玩家有一个特定的速度值,或者如果他们在商店中获得了升级,PlayerSpawner脚本将获取这些值并将它们应用到Player脚本上。
以下图表显示了游戏框架中PlayerSpawner类的部分视图及其与其他类的关联:

图 2.36 – PlayerSpawner UML
如我们所见,PlayerSpawner脚本与四个其他脚本相连:
-
Player:PlayerSpawner与Player相连,因为它创建了玩家。 -
SOActorModel:这是一个ScriptableObject,它为PlayerSpawner提供其值,这些值随后传递给Player。 -
IActorTemplate:这是通用于其他常见功能的interface。 -
GameManager:这将从PlayerSpawner脚本发送和接收一般游戏信息。
在我们创建PlayerSpawner脚本之前,创建一个空的游戏对象来存储与我们的玩家、他们的子弹以及玩家可能在testLevel场景中创建的其他任何东西有关的内容是很好的管理。
按照以下步骤创建并命名游戏对象:
-
右键点击打开空间中的层次结构窗口。
-
将出现一个下拉列表。从列表中选择创建空对象。
-
将游戏对象命名为
_Player。
这就是我们需要做的全部。现在,让我们从PlayerSpawner脚本开始:
-
在
Assets/Scripts文件夹中,文件名为PlayerSpawner。 -
打开脚本,确保我们在脚本顶部输入以下库:
using UnityEngine;
我们只需要using UnityEngine,因为它涵盖了脚本中需要的所有对象。
-
继续确保我们的类如下标记:
public class PlayerSpawner : MonoBehaviour {
在 Unity 中,继承MonoBehaviour是常见的,以便在 Unity 中为脚本提供更多功能。它的常见目的是让脚本可以附加到游戏对象上。
-
继续输入脚本的变量:
SOActorModel actorModel; GameObject playerShip;
在PlayerSpawner类内部,我们添加两个全局变量:第一个变量是actorModel,它包含一个可脚本化对象资产,该资产将包含玩家飞船的值,第二个变量将持有从我们的CreatePlayer方法创建的玩家飞船。
-
继续通过输入脚本的
Start函数:void Start() { CreatePlayer(); }
在全局变量之后,我们添加一个Start函数,该函数将在运行时PlayerSpawner脚本所持有的游戏对象激活时自动运行。
在Start函数的作用域内,有一个我们将要创建的方法,称为CreatePlayer。
-
继续通过输入
CreatePlayer方法:void CreatePlayer() { //CREATE PLAYER actorModel = Object.Instantiate(Resources.Load ("Player_Default")) as SOActorModel; playerShip = GameObject.Instantiate(actorModel.actor) as GameObject; playerShip.GetComponent<Player>().ActorStats(actorModel); //SET PLAYER UP } }
我将CreatePlayer方法分为两个注释部分(//CREATE PLAYER和//SET PLAYER UP),因为其大小。
CreatePlayer方法的第一部分将实例化玩家飞船的ScriptableObject资产,并将其存储在actorModel变量中。然后,我们实例化一个游戏对象,该对象引用我们的ScriptableObject,其中包含名为actor的游戏对象,位于名为playerShip的游戏对象变量中。最后,我们将我们的ScriptableObject资产应用到Player组件脚本中的ActorStats方法(我们将在本章后面创建)。
-
继续在
CreatePlayer方法中添加第二部分://SET PLAYER UP playerShip.transform.rotation = Quaternion.Euler(0,180,0); playerShip.transform.localScale = new Vector3(60,60,60); playerShip.name = "Player"; playerShip.transform.SetParent(this.transform); playerShip.transform.position = Vector3.zero;
在CreatePlayer方法的第二部分,我们在注释//SET PLAYER UP的位置添加了更多代码。
从//SET PLAYER UP开始的代码专门用于在关卡开始时将玩家的飞船设置在正确的位置。
代码执行以下操作:
-
设置玩家飞船的旋转方向。
-
将玩家飞船的缩放设置为所有轴上的
60。 -
当我们
实例化任何游戏对象时,Unity 会在游戏对象名称的末尾添加(Clone)。我们可以将其重命名为Player。 -
我们在层次结构窗口中,将
playerShip游戏对象设置为_Player游戏对象的孩子,这样我们就可以轻松找到它。 -
最后,我们重置玩家飞船的位置。
这就是我们的 PlayerSpawner 脚本编写完成。现在,在下一节中,我们需要创建并将此脚本附加到游戏对象上并为其命名。确保在继续之前保存脚本。
创建 PlayerSpawner 游戏对象
在本节中,我们将创建一个游戏对象,它将包含我们刚刚创建的 PlayerSpawner 脚本,然后我们将 PlayerSpawner 游戏对象放置在 testLevel 场景中。
要创建和设置我们的 PlayerSpawner 游戏对象,我们需要执行以下操作:
-
在
PlayerSpawner中。 -
将
PlayerSpawner游戏对象拖放到_Player(记住_Player是我们场景中的空游戏对象)游戏对象上,使其成为PlayerSpawner的子对象。
由于我们的 PlayerSpawner 游戏对象没有应用任何视觉元素,我们可以给它一个图标。
- 在 检查器 窗口中仍然选中
PlayerSpawner游戏对象,点击其名称左侧的多彩方块。将提供一系列颜色选项,如以下截图所示:

图 2.37 – 为 PlayerSpawner 选择图标
-
选择一个颜色。现在,
PlayerSpawner游戏对象将被赋予一个标签,以显示它在场景中的位置。这将现在出现在 场景 窗口中。小贴士
如果你仍然在 场景 窗口中看不到图标,请确保 3D 图标 已关闭。你可以通过点击 场景 窗口右上角的 ** Gizmos** 按钮并取消选中 3D 图标 复选框来检查。
在 _Player 游戏对象内部的 PlayerSpawner 游戏对象中,以下值:
- 在
PlayerSpawner游戏对象仍然被选中时,在 检查器 窗口中,给它以下 变换 值:

图 2.38 – 检查器窗口中的 PlayerSpawner 变量
-
在
PlayerSpawner中继续操作,直到你在下拉列表中看到脚本出现。 -
点击
PlayerSpawner脚本来将其添加到PlayerSpawner游戏对象中。
我们还不能移动飞船,也不能开火,因为我们还没有编写这部分代码。在下一节中,我们将介绍玩家的控制方式,然后我们将继续编写玩家的代码以及子弹在屏幕上移动的代码。
设置我们的输入管理器
记住这是一个横向卷轴射击游戏,所以控制将是二维的,尽管我们的视觉效果是三维的。我们现在的重点是设置 Players 的控制。为此,我们需要访问 输入管理器:
- 选择 编辑,然后选择 项目设置,然后从列表中选择 输入管理器:

图 2.39 – 在 Unity 编辑器中选择输入管理器
输入管理器将提供我们游戏所有可用控制器的列表。我们首先检查默认设置的控制。这里有很多选项,但如前所述,我们只需要浏览对我们有意义的属性,即以下属性:
-
水平:沿着玩家的 x 轴移动玩家的飞船
-
垂直:沿着玩家的 y 轴移动玩家的飞船
-
Fire1:使玩家开火
要检查这三个属性,我们需要做以下几步:
-
通过点击其旁边的箭头展开 轴 下拉菜单。
-
如下截图所示,展开 水平:

图 2.40 – 输入管理器
-1),右键配置为正数(+1)。其他具有相同效果的按键是左边的 A 和右边的 D。
如果我们有类似摇杆或方向盘的模拟控制,那么当玩家释放控制并返回中心时,我们可能需要关注重力的影响。死点指的是模拟控制器的中心。有时,控制器可能不平衡,并且自然倾向于一侧,因此通过增加死区,我们可以消除玩家可能检测到的虚假反馈。
-
-1),而正数按钮向上(+1)。其他按钮是向下的 S 和向上的 W。 -
mouse 0(即左鼠标按钮)。目前,从替代按钮中移除mouse 0。
要了解更多关于 输入管理器 窗口的信息,请点击 输入管理器 面板右上角的蓝色小书。
我们现在在 Player 脚本中设置了控制,以利用这些控制。
设置我们的玩家脚本
Player 脚本将被附加到玩家飞船游戏对象上,玩家将能够移动和开火,以及造成和承受伤害。我们还将确保玩家飞船不会超出剧本区域。在我们继续之前,让我们提醒自己 Player 脚本在我们游戏框架中的位置:

图 2.41 – 玩家 UML
Player 脚本将与以下脚本进行交互:
-
PlayerBullet:Player脚本将创建子弹进行射击。 -
PlayerSpawner:Player脚本由PlayerSpawner创建。 -
IActorTemplate:包含伤害控制和Player的属性。 -
GameManager:存储额外的信息,如生命值、分数、等级以及玩家飞船积累的任何升级。 -
SOActorModel:存储ScriptableObject的Player属性。
现在我们熟悉了 Player 脚本与其他脚本的关系,我们可以开始编写它:
-
在
Assets/Scripts文件夹中,文件名为Player。 -
打开脚本,并将
IActorTemplate接口添加到现有的默认代码中:using UnityEngine; public class Player : MonoBehaviour, IActorTemplate {
默认情况下,脚本将导入一个UnityEngine库(包括其他一些库),类的名称,以及MonoBehaviour。所有这些对于使脚本在 Unity 编辑器中工作都是必需的。
-
继续在
Player脚本中输入以下全局变量:int travelSpeed; int health; int hitPower; GameObject actor; GameObject fire; public int Health { get {return health;} set {health = value;} } public GameObject Fire { get {return fire;} set {fire = value;} } GameObject _Player; float width; float height;
我们在我们的全局变量中输入了整数、浮点数和游戏对象混合体;从顶部开始,前六个变量将从玩家的SOActorModel脚本中更新。travelSpeed是玩家飞船的速度,health是玩家在死亡之前可以承受的打击次数,hitPower是飞船在撞击可以承受伤害的物体(敌人)时造成的伤害,actor是用于表示玩家的三维模型,最后,fire变量是玩家发射的三维模型。如果这看起来有点仓促,请回到介绍我们的 ScriptableObject – SOActorModel部分,在那里我们更详细地介绍了这些变量。
Health和Fire这两个公共属性存在是为了让其他需要访问的类能够访问我们的两个private health和fire变量。
_Player变量将用于引用场景中的_Player游戏对象。
最后两个变量width和height将用于存储游戏在游戏中使用的屏幕世界空间尺寸的测量结果。我们将在下一块代码中进一步讨论这两个变量。
在我们开始以下Start函数代码块之前,你可能想知道为什么在运行函数代码内容时我们会选择Start而不是Awake。这两个函数在运行时都只运行一次;唯一明显的区别是Awake在对象创建时运行。Start在对象启用时执行,如docs.unity3d.com/Manual/ExecutionOrder.html上的文档所示。
为了简化我们的 Unity 项目,我们将在这两个函数之间进行选择。这是为了避免多个Awake函数同时运行时的冲突。例如,一个脚本可能试图更新其 Text UI,但更新文本的变量在运行时可能仍然是 null,因为包含该变量的脚本仍在等待其内容更新。
通过转到 Unity 的脚本执行顺序在编辑 | 项目设置 | 脚本执行顺序,可以避免在运行时由多个脚本调用多个Awake函数之间的冲突。
如果你想了解更多关于脚本执行顺序的信息,请查看docs.unity3d.com/Manual/class-MonoManager.html上的文档。
-
继续在
Player脚本中输入代码,接下来,我们将输入Start函数及其内容:void Start() { height = 1/(Camera.main.WorldToViewportPoint (new Vector3(1,1,0)).y - .5f); width = 1/(Camera.main.WorldToViewportPoint(new Vector3(1,1,0)) .x - .5f); _Player = GameObject.Find("_Player"); }
如前所述,height和width变量将存储我们的世界空间测量值。这些是必需的,以便我们可以将玩家的飞船限制在屏幕内。高度和宽度代码行使用类似的方法;唯一的区别是我们读取的轴。
Camera.main组件指的是场景中的摄像机,它使用的函数WorldToViewportPoint是将游戏的三维世界空间的结果转换为视口空间。如果你不确定视口空间是什么,它类似于我们知道的屏幕分辨率,只是其测量单位是点而不是像素,这些点是从0到1进行测量的。以下图表显示了屏幕与视口测量的比较:

图 2.42 – 屏幕与视口测量
因此,使用视口,无论屏幕的分辨率如何,完整的高度和宽度都是1,而所有介于其间的都是分数。所以,对于高度,我们向WorldToViewportPoint提供Vector3,其中Vector3表示世界空间值,后面跟着-0.5f,将其偏移量设置回0。然后,我们将1(这是我们全屏大小)除以我们公式的结果。这将给我们当前屏幕的世界空间高度。然后,我们应用相同的原则来处理宽度,使用x而不是y,并存储结果。
最后,代码的最后一行将场景中_Player游戏对象的引用存储到我们的变量中。
-
继续使用
Player脚本,我们有一个在每一帧被调用的Update函数。输入函数以及以下两个方法:void Update () { //Movement(); //Attack(); }
Update函数在每一帧运行Movement方法和Attack方法。我们将在本章后面深入探讨这两个方法,现在我们将这两个方法注释掉("//"),以避免脚本无法运行。
我们接下来要放入Player脚本中的下一个方法是ActorStats方法。这个方法是必需的,因为我们声明了我们在继承的接口中。
-
在我们的
Update函数作用域之后,输入以下代码段:public void ActorStats(SOActorModel actorModel) { health = actorModel.health; travelSpeed = actorModel.speed; hitPower = actorModel.hitPower; fire = actorModel.actorsBullets; }
我们刚刚输入的代码将分配来自玩家SOActorModel ScriptableObject资产中的值,这是我们本章早期制作的。
这种方法不会在我们的脚本中运行,而是由其他类访问,原因在于这些变量持有关于我们的玩家的值,并且不需要在任何其他地方。
- 保存
Player脚本。
在我们测试到目前为止的内容之前,我们需要在项目窗口中将我们的Player脚本附加到player_ship上。
-
在
Assets/Prefab中,选择player_ship预制体。 -
选择
Player,直到脚本出现,然后选择它。
使用我们的_Player、PlayerSpawner和GameManager游戏对象,现在是时候测试游戏了。我们可以在编辑器中按播放键看到玩家飞船在我们的游戏窗口中创建。
以下截图显示了我们的游戏中的PlayerSpawner游戏对象作为Player游戏对象的父亲;同时注意PlayerSpawner图标:

图 2.43 – 我们游戏中当前的玩家设置
小贴士
在进入下一节之前,通过将PlayerSpawner游戏对象拖放到Assets/Player中创建一个预制体。这样,如果你因为任何原因丢失了场景及其层次结构内容,你可以将你的预制体拖回。这应该适用于任何常见的活动游戏对象。
让我们进入下一节,我们将继续在Player脚本上工作,但这次,我们将查看当玩家的游戏对象与敌人接触时会发生什么。
与敌人碰撞 – OnTriggerEnter
在本节中,我们将在Player脚本中添加一个函数,用于在运行时检查我们的玩家游戏对象发生了什么碰撞。目前,唯一可以与我们的玩家发生碰撞的是敌人,但我们仍然可以展示 Unity 自带的OnTriggerEnter函数的使用,该函数为我们处理了大部分工作:
-
在
Player脚本中我们的上一个方法(ActorStats)的作用域之后继续,我们将添加以下代码,用于检测我们的敌人是否与玩家的飞船发生碰撞:void OnTriggerEnter(Collider other) { if (other.tag == "Enemy") { if (health >= 1) { if (transform.Find("energy +1(Clone)")) { Destroy(transform.Find("energy +1(Clone)"). gameObject); health -= other.GetComponent<IActorTemplate> ().SendDamage(); } else { health -= 1; } } if (health <= 0) { Die(); } } }
让我们解释一下我们刚刚输入到Player脚本中的部分代码:
-
OnTriggerEnter(Collider other)是一个 Unity 识别的函数,用于检查什么进入了玩家的触发碰撞器。 -
我们使用一个
if语句来检查碰撞器的tag是否被命名为Enemy。注意当我们创建我们的敌人时,我们将给他们一个Enemytag,这样它们就容易被识别。如果tag等于Enemy,我们就将其放入那个if语句中。 -
下一个
if语句检查玩家的health是否等于或大于1。如果是,这意味着玩家可以承受打击并继续游戏而不会死亡,同时也意味着我们可以进入其if语句。 -
我们接近第三个
if语句,该语句检查碰撞器是否有一个名为energy +1(Clone)的游戏对象。这个对象的名称是玩家可以在游戏商店购买的护盾的名称,我们将在第六章,购买游戏内物品和广告中添加。如果玩家有这个energy +1(Clone)对象,我们可以使用 Unity 的预制函数Destroy它。我们还从敌人的SendDamage函数中扣除玩家的额外生命值。我们将在本章后面讨论SendDamage。 -
在第三个
if语句之后是一个else条件,其中,如果玩家没有energy +1(Clone)游戏对象,他们的健康值会被扣除。 -
最后,如果玩家的
health值为零或以下,我们将运行Die方法,我们将在本章后面介绍。小贴士
在我们继续向项目中添加更多代码的同时,不要忘记保存你的工作。
让我们继续我们的 Player 脚本并添加功能,以便玩家可以从敌人那里接收和发送伤害。
- 在下一个方法中,我们将添加两个方法。第一个方法 (
TakeDamage) 将接受一个名为incomingDamage的整数,并使用该值从我们的玩家health值中扣除。
第二个方法 (SendDamage) 将返回我们的 hitPower 值的整数。
-
在我们的
ActorStats方法下方和范围之外,现在添加以下代码:public void TakeDamage(int incomingDamage) { health -= incomingDamage; } public int SendDamage() { return hitPower; }
让我们继续另一个 Player 脚本的方法,并使其能够控制玩家飞船在 游戏 窗口周围移动。
移动方法
在本节中,我们将编写 Movement 方法,它将从玩家的摇杆/键盘接收输入,并利用 height 和 width 浮点数来确保玩家的飞船保持在屏幕内:。
-
仍然在
Player脚本中,使用以下内容开始以下方法以检查玩家的输入:void Movement() { if (Input.GetAxisRaw("Horizontal") > 0) { if (transform.localPosition.x < width + width/0.9f) { transform.localPosition += new Vector3 (Input.GetAxisRaw("Horizontal") *Time.deltaTime*travelSpeed,0,0); } }-
Movement方法将包括检测玩家从四个方向进行的移动;我们将从玩家按下控制器/键盘上的右键开始。我们运行一个if语句来检查输入管理器是否检测到来自Horizontal属性的任何移动。如果GetAxisRaw检测到一个大于零的值,我们就会进入if语句的条件。请注意,GetAxisRaw没有平滑处理,所以除非添加额外的代码,否则玩家的飞船会立即移动。 -
接下来,我们还有一个
if语句;这个语句检查玩家是否超过了width(即我们之前在章节中计算的屏幕世界空间)。我们还添加了一个额外的部分width以避免玩家的飞船几何形状离开屏幕。如果玩家的位置仍然低于width(及其缓冲区)值,我们将在if语句内运行内容。 -
玩家的位置通过一个
Vector3结构更新,它包含Horizontal方向的值,乘以每帧的时间以及我们从ScriptableObject设置的travelSpeed。
-
-
让我们在
Movement方法中继续,并添加一个类似的if语句以将玩家飞船向左移动:if (Input.GetAxisRaw("Horizontal") < 0) { if (transform.localPosition.x > width + width/6) { transform.localPosition += new Vector3 (Input.GetAxisRaw("Horizontal") *Time.deltaTime*travelSpeed,0,0); } }
如我们所见,代码接近之前的块;唯一的区别是,我们的第一个 if 语句检查我们是否在向左移动;第二个 if 语句检查玩家的位置是否大于宽度以及一个略微不同的缓冲区。
除了这些,if语句及其内容在相反的方向上服务于相同的位置。
-
让我们继续使用我们的
Movement方法,并添加移动玩家飞船下方的if语句代码:if (Input.GetAxisRaw("Vertical") < 0) { if (transform.localPosition.y > -height/3f) { transform.localPosition += new Vector3 (0,Input.GetAxisRaw("Vertical")*Time.deltaTime*travelSpeed,0); } }
再次强调,我们遵循与前面两个if语句相同的规则,但这次,我们添加了Vertical string属性,而不是Horizontal。在第二个if语句中,我们检查玩家的 y 轴是否高于负的height/3。我们之所以除以这个值,是因为在本书的后面部分(第九章**,创建 2D 商店界面和游戏内 HUD),我们将在屏幕底部添加图形,这将限制玩家的视野。
-
让我们继续到
Movement方法中的最后一个if语句,即向上移动:if (Input.GetAxisRaw("Vertical") > 0) { if (transform.localPosition.y < height/2.5f) { transform.localPosition += new Vector3 (0,Input.GetAxisRaw("Vertical")*Time.deltaTime*travelSpeed,0); } } }
与之前一样,这个if语句承担着类似的角色,但这次,它检查的是玩家的位置是否低于height/2.5f值。应用了一个缓冲区以防止三维几何体离开屏幕顶部。
小贴士
在制作游戏时,有时会遇到玩家斜向移动时速度增加的情况。这是因为玩家实际上同时按下了两个方向,而不是一个方向。
为了确保一个方向只有1的量级,我们可以使用 Unity 预制的Normalize函数。
要了解更多关于这个函数的信息,请查看docs.unity3d.com/ScriptReference/Vector3.Normalize.html上的文档。
- 不要忘记保存脚本。
我们将继续通过添加Die方法来完善Player脚本。
Die方法
将Die方法添加到Player脚本中,将使我们的玩家可以被摧毁。目前,在Die方法中有一个 Unity 函数叫做Destroy;这个函数将删除其参数内的任何游戏对象。
将以下方法输入到Player脚本中,以摧毁玩家:
public void Die()
{
Destroy(this.gameObject);
}
让我们继续到Player脚本中的最后一个方法,即攻击方法。
Attack方法
在本节中,我们将向Player脚本中的Attack方法添加内容。
这个Attack方法的目的是从玩家那里接收输入,创建子弹,将子弹指向正确的方向,并将子弹设置为Player游戏对象的子对象,以保持我们的层次结构窗口整洁。
将以下Attack方法输入到Player脚本中,以允许玩家开火:
public void Attack()
{
if (Input.GetButtonDown("Fire1"))
{
GameObject bullet = GameObject.Instantiate
(fire,transform.position,Quaternion.Euler
(new Vector3(0, 0, 0))) as GameObject;
bullet.transform.SetParent(_Player.transform);
bullet.transform.localScale = new Vector3(7,7,7);
}
}
在Attack方法内部,我们调用一个if语句来检查玩家是否按下了Fire1按钮(Windows 上的左 Ctrl;如果你使用的是 Mac,则是command)。如果玩家按下了Fire1按钮,我们将进入if语句的作用域。
注意
当开发者提到函数的作用域、if 语句、类等时,他们指的是大括号开闭之间的内容。例如,如果以下代码中的 money 变量值更高,下面的 if 语句将会执行:
if (money > costOfPizza)
{
//两个大括号顶部和底部之间发生的事情都在 if 语句的作用域内。
}
在 if 语句内部,我们再添加一个 if 语句以确保在点击鼠标时,我们点击的是屏幕而不是任何 UI 相关的内容。当我们在 第十章 中查看添加暂停按钮、更改音效和模拟测试时,这会变得更加相关。如果我们点击了任何 UI 相关的内容,我们调用 return,这意味着我们退出 if 语句,这样就不会发射子弹。
由于我们已经输入了移动和攻击函数的内容,我们可以滚动回 Update 函数并移除我们添加的注释。
我们的 Update 函数现在看起来如下所示:
void Update()
{
Movement();
Attack();
}
接下来,我们从其实例名称 fire 中 Instantiate 我们的 PlayerBullet 游戏对象。我们还使 fire 游戏对象相对于屏幕向右,并移动它以朝向迎面而来的敌人。我们将创建和定位我们的游戏对象的结果存储在一个名为 bullet 的变量中。
然后,我们将子弹的大小设置为原始大小的七倍,使其看起来更大。
最后,在 if 语句内部,我们让我们的 bullet 游戏对象位于一个名为 _Player 的单个游戏对象内。
那就是为 Player 脚本所需的全部代码!在继续之前,请确保保存脚本。
在下一节中,我们将继续到另一个玩家脚本,该脚本控制玩家发射子弹时发生的情况。
设置我们的 PlayerBullet 脚本
在本节中,我们将创建一个子弹,它将从玩家的飞船穿越屏幕。
你会注意到 PlayerBullet 脚本与 Player 脚本非常相似,因为它携带了 IActorTemplate 和 SOActorModel 脚本,这些脚本已经编码在 Player 脚本中。
让我们创建我们的 PlayerBullet 脚本:
-
在
Assets/Scripts文件夹中,文件名为PlayerBullet。 -
打开脚本,并在脚本顶部检查/输入以下代码:
using UnityEngine;
如前所述,默认情况下,我们需要 UnityEngine 库。
-
让我们继续检查正确的类名并输入以下继承:
public class PlayerBullet : MonoBehaviour, IActorTemplate {
我们声明 public 类并默认继承 MonoBehaviour。我们还继承 IActorTemplate 接口,以便从其他游戏对象脚本(如 SendDamage 和 TakeDamage)中获取与游戏对象相关的函数。
-
将以下全局变量输入到
PlayerBullet脚本中:GameObject actor; int hitPower; int health; int travelSpeed; [SerializeField] SOActorModel bulletModel;
我们添加的所有变量都是 private。最后一个变量添加了 SerializeField 属性。SerializeField 使得这个变量在 private 中可见,我们仍然可以将资产拖放到其字段中(我们将在稍后这样做)。有关 SerializeField 属性的更多信息,请参阅 docs.unity3d.com/ScriptReference/SerializeField.html。
-
接下来,我们将进入
Awake函数及其内容:void Awake() { ActorStats(bulletModel); }
在我们的 Awake 函数中是 ActorStats 方法,这是必需的,因为我们正在继承一个声明它的 interface。
-
继续输入
SendDamage和TakeDamage方法:public int SendDamage() { return hitPower; } public void TakeDamage(int incomingDamage) { health -= incomingDamage; }
如本章已提到的,我们需要这些方法来发送和接收伤害。
-
继续前进,我们进入
Die方法及其内容:public void Die() { Destroy(this.gameObject); }
从我们的 interface 中包含的另一个方法是 Die 方法。
-
接下来,进入
ActorStats方法:public void ActorStats(SOActorModel actorModel) { hitPower = actorModel.hitPower; health = actorModel.health; travelSpeed = actorModel.speed; actor = actorModel.actor; }
我们从 interface 继承的最后一个方法是 ActorStats 方法,它将包含我们的 ScriptableObject 资产。然后,这个资产将被分配给 PlayerBullet 脚本的全局变量。
-
下一个函数是
OnTriggerEnter,以及其if语句条件检查,如下所示:void OnTriggerEnter(Collider other) { if (other.tag == "Enemy") { if(other.GetComponent<IActorTemplate>() != null) { if (health >= 1) { health -= other.GetComponent<IActorTemplate> ().SendDamage(); } if (health <= 0) { Die(); } } } }
在前面的代码块中,我们进行了一个检查,看看我们的子弹是否与被标记为 "Enemy" 的碰撞器发生了碰撞。如果碰撞器被标记为 "Enemy" 并指向玩家,我们接着检查该碰撞器是否包含 IActorTemplate 接口。如果没有,那么 "Enemy" 碰撞器可能是一个障碍物。否则,我们从 "Enemy" 游戏对象中扣除 health 并检查它是否已死亡。
-
现在,让我们进入子弹移动的 Unity
Update函数:void Update () { transform.position += new Vector3(travelSpeed,0,0)*Time.deltaTime; }
Update 函数根据其 travelSpeed 值乘以 Time.deltaTime(Time.deltaTime 是从上一帧开始的秒数)在每一帧向其 x 轴添加值。
重要提示
如果你想了解更多关于 Time.deltaTime 的信息,请查看文档 docs.unity3d.com/ScriptReference/Time-deltaTime.html。
-
接下来,进入 Unity 的
OnBecameInvisible函数:void OnBecameInvisible() { Destroy(gameObject); } }
最后一个函数将移除任何已离开屏幕的不必要子弹。这将有助于提高我们游戏的表现并保持其整洁。在继续之前,请确保已保存脚本。
接下来,我们需要将 PlayerBullet 脚本应用到我们的 player_bullet 预制件上:
-
导航到
Assets/Prefab/Player并选择player_bullet。 -
在选择
Player_Bullet后,点击PlayerBullet直到看到PlayerBullet脚本。 -
选择脚本,并将
PlayerBullet资产从 Bullet Model 字段添加到其中(将资产拖放到字段中或点击其字段右侧的远程按钮)。
以下截图显示了带有脚本和资产的 player_bullet:

图 2.44 – 检查器窗口中的 player_bullet 组件
我们现在可以继续下一节,关于为玩家攻击而创建敌人的内容!
规划和创建我们的敌人
我们有一个可以移动、射击和承受伤害的玩家;我们现在可以开始考虑创建一个具有这些属性的敌人。
为了提醒自己我们正在制作的类型,我们的游戏具有与经典街机射击游戏(如 Konami 的Gradius、Capcom 的UN Squadron和 Irem 的R-Type)相同的特征(github.com/retrophil/Unity-Certified-Programmer-Exam-Guide-2nd-Edition/blob/main/Reference/shootEmUps.png)。通常,在这类游戏中,玩家会被从屏幕右侧涌来的敌人包围,并从左侧退出。
在本节中,我们将重复PlayerSpawner和Player脚本的一些类似方面。EnemySpawner脚本需要调整,以便以一定速率实例化一定数量的敌人飞船。
Enemy游戏对象将自行移动,因此需要对其行为应用一些额外的代码。在我们开始创建第一个敌人脚本之前,让我们看看我们的游戏框架的一部分,并注意布局基本上与游戏框架的玩家端相同:

图 2.45 – EnemySpawner 和 Enemy UML
在我们跳入EnemySpawner脚本之前,让我们做与我们的玩家游戏对象相同的清理工作,即创建一个空的游戏对象并将所有相关的游戏对象存储在该游戏对象中。我们这样做是为了在层次结构窗口中去除杂乱,所以让我们为我们的敌人也这样做:
-
在层次结构窗口的空白区域右键点击。
-
将出现一个下拉列表;选择创建空对象。
-
将游戏对象命名为
_Enemies。
让我们继续编写敌人脚本。
设置我们的 EnemySpawner 和 Enemy 脚本
在本节中,我们将开始编写我们的EnemySpawner脚本和游戏对象。EnemySpawner脚本的目的是在设定速率下多次生成一个敌人游戏对象。一旦我们的testLevel场景开始,我们的敌人生成器将开始释放敌人。然后敌人将移动到屏幕的左侧。这相当简单,正如前一小节简要提到的,EnemySpawner使用与PlayerSpawner相同的interface和可脚本化的对象来实例化敌人。让我们首先创建我们的EnemySpawner脚本:
-
在
Assets/Scripts文件夹中,文件名为EnemySpawner。 -
打开脚本并输入以下代码:
using System.Collections; using UnityEngine;
如往常一样,我们使用默认的UnityEngine库。
我们还将使用另一个名为 System.Collections 的库。当我们使用协程时,需要这个库,这将在本节后面解释。
-
接下来,我们将检查/输入类名及其继承:
public class EnemySpawner : MonoBehaviour {
确保类名为 EnemySpawner,并且它默认继承自 MonoBehaviour。
-
接下来,向
EnemySpawner脚本添加四个全局变量:[SerializeField] SOActorModel actorModel; [SerializeField] float spawnRate; [SerializeField] [Range(0,10)] int quantity; GameObject enemies;
在前面的代码中输入的所有变量都具有 private 的可访问级别,除了 enemies 变量之外的所有变量都应用了 SerializeField 和 Range 属性,范围在 0 到 10 之间。这样做的原因是,我们可以或其他设计师可以轻松地从 Inspector 窗口中更改 EnemySpawner 的生成速率和敌人数量,如下面的截图所示:



-
现在,让我们输入 Unity 的
Awake函数和一些内容:void Awake() { enemies = GameObject.Find("_Enemies"); StartCoroutine(FireEnemy(quantity, spawnRate)); }
在 Awake 函数中,我们从一个空的 _Enemies 游戏对象分隔符创建一个实例,并将其存储在 enemies 变量中。
在我们的 Awake 函数中的第二行代码是一个 StartCoroutine。
重要信息
StartCoroutine() 和 IEnumerator 两者相辅相成。它们的作用类似于一个方法,接受参数并在其中运行代码。与协程的主要区别在于,它们可以被帧更新或时间延迟。你可以把它们看作是 Unity 自身 Invoke 函数的更高级版本。
要了解更多关于协程以及如何在 IEnumerator 实例中实现它们的信息,请查看 Unity 的文档:docs.unity3d.com/ScriptReference/MonoBehaviour.StartCoroutine.html。
这将用于运行我们创建敌人的方法,但正如你可能也注意到的,它接受两个参数。第一个是它所包含的敌人数量,第二个是 spawnRate,它延迟每个生成的敌人。
-
接下来,在我们的
EnemySpawner脚本中,我们有FireEnemy,它将被用来运行创建和定位每个敌人的循环,然后等待重复该过程。 -
接下来,在
Awake函数下方和外部,我们可以添加我们的IEnumerator:IEnumerator FireEnemy(int qty, float spwnRte) { for (int i = 0; i < qty; i++) { GameObject enemyUnit = CreateEnemy(); enemyUnit.gameObject.transform.SetParent(this.transform); enemyUnit.transform.position = transform.position; yield return new WaitForSeconds(spwnRte); } yield return null; }
在 FireEnemy IEnumerator 中,我们启动一个 for 循环,该循环将遍历其 qty 值。
在 for 循环内,添加以下内容:
-
我们还没有介绍的一个方法,称为
CreateEnemy。CreateEnemy的结果将被转换成一个名为enemyUnit的游戏对象实例。 -
enemyUnit是从EnemySpawner游戏对象中飞出的敌人。 -
我们将
EnemySpawner位置分配给我们的enemyUnit。 -
我们等待
spwnRte值所设置的秒数。 -
最后,这个过程会一直重复,直到
for循环达到其总数。
-
最后,在
FireEnemyIEnumerator下方和外部添加以下方法:GameObject CreateEnemy() { GameObject enemy = GameObject.Instantiate(actorModel.actor) as GameObject; enemy.GetComponent<IActorTemplate>().ActorStats(actorModel); enemy.name = actorModel.actorName.ToString(); return enemy; } }
正如我们提到的,有一个名为 CreateEnemy 的方法。除了显而易见的功能外,此方法还将执行以下操作:
-
从其
ScriptableObject资产中Instantiateenemy游戏对象。 -
从其
ScriptableObject资产中为我们的敌人应用值。 -
从其
ScriptableObject资产中为敌人游戏对象命名。
不要忘记保存脚本。
我们现在可以继续到下一节,我们将创建并准备带有其游戏对象的 EnemySpawner。
将我们的脚本添加到敌人生成器游戏对象
最后,我们需要将我们的 EnemySpawner 脚本附加到一个空的游戏对象上,这样我们就可以在 testLevel 场景中使用它。要设置 EnemySpawner 游戏对象,请执行以下操作:
-
创建一个空的游戏对象并将其命名为
EnemySpawner。 -
正如我们处理
_Player和PlayerSpawner一样,我们需要在 层次结构 窗口中将EnemySpawner游戏对象移动到_Enemies游戏对象内部。 -
将
EnemySpawner游戏对象移动到_Enemies游戏对象后,我们现在需要在 检查器 窗口中更新EnemySpawner游戏对象的 变换 属性值:

图 2.47 – 在检查器窗口中敌人生成器的变换值
- 仍然在
EnemySpawner中,直到你在列表中看到它,然后点击它。
此外,为了在 EnemySpawner 游戏对象中提供视觉辅助,就像我们在 创建玩家生成器游戏对象 部分中所做的那样。
以下截图显示了给我的 EnemySpawner 分配的图标:

图 2.48 – 敌人生成器图标
我们现在可以在 检查器 窗口中将一个敌人添加到我们的 EnemySpawner 游戏对象中,并使用其脚本:

图 2.49 – 持有基本波敌人演员的敌人生成器组件
我们现在可以继续到下一节,创建我们的敌人脚本。
设置我们的敌人脚本
就像我们的玩家飞船是从 PlayerSpawner 创建出来的一样,我们的第一个敌人将是从其 EnemySpawner 创建出来的。敌人脚本将包含类似的变量和函数,但它还将有自己的移动,类似于 PlayerBullet 沿其 x 轴移动。
让我们开始创建我们的敌人脚本:
-
在
Assets/Scripts文件夹中,文件名为EnemyWave。 -
打开脚本,并在脚本顶部检查/输入以下所需的库代码:
using UnityEngine;
就像我们的大多数类一样,我们需要 UnityEngine 库。
-
检查并输入类名及其继承:
public class EnemyWave : MonoBehaviour, IActorTemplate {
我们有一个名为 EnemyWave 的 public class,默认继承自 MonoBehaviour,但还添加了我们的 IActorTemplate 接口。
-
在
EnemyWave类中,输入以下全局变量:int health; int travelSpeed; int fireSpeed; int hitPower; //wave enemy [SerializeField] float verticalSpeed = 2; [SerializeField] float verticalAmplitude = 1; Vector3 sineVer; float time;
EnemyWave类的全局变量是前四个变量,这些变量使用其ScriptableObject资产中的值进行更新。其他变量是针对特定敌人的,我们为其中两个变量赋予了SerializeField属性,以便在检查器窗口中进行调试。
-
添加 Unity 的
Update函数及其内容:void Update () { Attack(); }
在全局变量之后,我们添加一个包含Attack方法的Update函数。
-
添加我们的
ScriptableObject方法ActorStats及其内容:public void ActorStats(SOActorModel actorModel) { health = actorModel.health; travelSpeed = actorModel.speed; hitPower = actorModel.hitPower; }
我们有一个ActorStats方法,它接受一个ScriptableObject SOActorModel。然后,这个ScriptableObject应用它持有的变量值,并将它们应用到EnemyWave脚本的变量中。
-
仍然在
EnemyWave脚本中,添加Die方法及其内容:public void Die() { Destroy(this.gameObject); }
如果您一直跟随着,另一个熟悉的方法是Die方法,该方法在玩家摧毁敌人时被调用。
-
将 Unity 的
OnTriggerEnter函数添加到EnemyWave脚本中:void OnTriggerEnter(Collider other) { // if the player or their bullet hits you. if (other.tag == "Player") { if (health >= 1) { health -= other.GetComponent<IActorTemplate> ().SendDamage(); } if (health <= 0) { Die(); } } }
Unity 自带的OnTriggerEnter函数将检查它们是否与玩家发生碰撞,如果是,将发送伤害,并且敌人将使用Die方法摧毁自己。
-
继续输入
TakeDamage和SendDamage方法:public void TakeDamage(int incomingDamage) { health -= incomingDamage; } public int SendDamage() { return hitPower; }
来自IActorTemplate接口的另一组常见方法是,从EnemyWave脚本发送和接收伤害。
接下来是Attack方法,它控制敌人的移动/攻击。该方法在每一帧的Update函数中被调用。
使用这个攻击,我们将使敌人以波浪动画(类似于蛇)的形式从右向左移动,而不是直接从右向左移动。以下图像显示了我们的敌人以波浪线从右向左移动:

Figure 2.50 – 敌人的波浪攻击模式
-
将以下
Attack方法代码输入到EnemyWave脚本中:public void Attack() { time += Time.deltaTime; sineVer.y = Mathf.Sin(time * verticalSpeed) * verticalAmplitude; transform.position = new Vector3(transform. position.x + travelSpeed * Time.deltaTime, transform.position.y + sineVer.y, transform.position.z); }}
Attack方法从收集在标记为time的float变量中的Time.deltaTime开始。
然后,我们使用 Unity 的一个预制函数,该函数返回一个正弦值(docs.unity3d.com/ScriptReference/Mathf.Sin.html),使用我们的time变量,乘以verticalSpeed变量中的设置速度,然后乘以结果乘以verticalAmplitude。
最终结果存储在Vector3 y轴上。这基本上会使我们的敌人飞船上下移动。verticalSpeed参数设置其速度,而verticalAmplitude则改变其上下移动的距离。
然后,我们执行与PlayerBullet类似的任务,使敌人飞船沿着x轴移动,并且我们还添加了正弦计算来改变其Y位置,使其上下移动。
在我们结束这一章之前,请确保保存脚本。
在我们总结之前,请在编辑器中点击播放,希望一切顺利的话,你将能够驾驶一艘飞船在游戏窗口的边界内飞行;敌人将会从屏幕右侧漂浮进入并向左移动;你将能够用你的子弹摧毁这些敌人。如果敌人与你接触,它们也会摧毁你。最后,我们的层次结构窗口在游戏前后都整洁且结构良好。以下截图展示了我所解释的内容:
![图 2.52 – 包含当前游戏玩法和层次结构游戏对象结构的游戏窗口]
![图 2.52 – Figure_2.52_B18381.jpg]
图 2.51 – 包含当前游戏玩法和层次结构游戏对象结构的游戏窗口
你已经做了很多!好消息是,你刚刚征服了本书中最大的章节之一——我知道这很巧妙。但我们已经有了我们游戏的核心,最重要的是,我们已经覆盖了 Unity 程序员考试的大部分内容。
可以理解的是,你可能已经在路上遇到了一些可能的问题,你可能感到卡住了。如果这种情况发生,请不要担心——检查本章的Complete文件夹以加载 Unity 项目,并将该文件夹中的代码与你的代码进行比较以进行双重检查。确保你的场景中有正确的游戏对象,检查正确的游戏对象是否被标记,检查你的球体碰撞器的半径大小,如果你在控制台窗口中看到任何错误或警告,双击它们,它们将带你到导致问题的代码。
让我们总结本章,并谈谈到目前为止我们的游戏。
摘要
我们已经完成了本章的内容,我们已经征服了我们游戏框架的大部分内容,正如我们可以在以下图表中看到的那样:
![图 2.53 – Killer Wave UML]
![图 2.53 – Figure_2.53_B18381.jpg]
图 2.52 – Killer Wave UML
我们创建了一个游戏框架,无论我们为游戏添加 1 个还是 1000 个更多的敌人,都需要进行很少的更改。这种使用可重用代码和ScriptableObject的一些好处是,它将使非程序员受益,节省时间,并防止合作者陷入代码的困境。
我们还使得,如果我们想要添加更多的EnemySpawner点,我们可以将更多的预制体拖放到我们的场景中,并更新其ScriptableObject来更改敌人,而无需在精确的Vector3位置编码。
我们已经涵盖了其他常见的 Unity 功能,包括实例化游戏对象,如敌人和玩家子弹。
在下一章中,我们将介绍以下脚本:
-
ScoreManager:当一个敌人被摧毁时,玩家将获得分数。 -
ScenesManager:如果玩家死亡,将扣除一条生命;如果玩家失去所有生命,则关卡将重置。 -
Sounds:我们的飞船和子弹也将添加声音。
最后,我们将更新我们代码的整体结构。
第三章:第三章:管理脚本和进行模拟测试
在本章中,我们将通过将 Singleton 设计模式应用到我们的GameManager脚本中来继续构建我们的游戏。这将允许我们的游戏在保持脚本管理器功能的同时进入另一个场景,并防止它们被清除(从而保留我们的数据)。然后我们将开始处理脚本的其他细节,并观察信息(如玩家的生命值)是如何通过游戏框架传递的。如果玩家死亡,生命值将减少。如果玩家失去所有生命值,将触发游戏结束场景。
我们将扩展我们的原始代码,并引入敌人得分点,这样当我们用子弹击中敌人时,敌人将像往常一样消失,同时也会生成得分。这种得分机制将由我们创建的新得分管理器来处理。
我们还将为玩家的子弹添加声音,这是一个简单的任务。这将使我们了解扩展和调整音频源,我们将在后面的章节中继续这一过程。
最后,我们将通过几个适合本书主题的问题来测试自己,为考试做准备。这些问题将涵盖我们已经学到的内容,如果你一直跟随本书学习,你将有很大机会通过考试。
到本章结束时,我们将扩展我们的游戏框架,为我们的游戏添加更多功能,并通过一些 Unity 考试问题来测试我们的知识。
在本章中,我们将涵盖以下主题:
-
添加Singleton设计模式
-
设置我们的
ScenesManager脚本 -
为玩家创建生命值
-
记录敌人击中得分
-
为玩家的子弹创建声音
-
模拟测试
下一节将介绍本章涵盖的核心考试技巧。
本章涵盖的核心考试技巧
编程核心交互:
- 实现和配置游戏对象行为和物理
编程场景和环境设计:
-
确定实现音频资源的脚本
-
识别实现游戏对象实例化、销毁和管理的方法
在专业软件开发团队中工作:
- 认识到结构脚本以实现模块化、可读性和可重用性的技术
技术要求
本章的项目内容可以在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_03找到。
您可以下载每个章节的项目文件的全部内容,请访问github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition。
本章的所有内容都保存在章节的unitypackage文件中,包括一个包含我们在本章中将要执行的所有工作的Complete文件夹。
查看以下视频以查看代码执行情况:bit.ly/3xW4Zte。
添加单例设计模式
如您所回忆的,在第一章,设置和构建我们的项目中,我们讨论了设计模式及其对我们代码维护的有用性。我们简要介绍的设计模式之一是单例模式。我们不重复自己,单例模式为我们提供了全局访问代码的能力,然后我们可以在游戏中的某个点获取这些代码。那么,我们可以在哪里看到使用单例设计模式的益处呢?嗯,我们可以用它来确保 Unity 始终保持某些脚本的可访问性,无论我们处于哪个场景。我们已经为我们的游戏框架添加了很多结构,我们仍然需要添加几个管理脚本,例如ScoreManager和ScenesManager。
现在是给所有管理脚本提供对游戏中所有其他脚本的全局访问权限的好时机。管理脚本提供了对正在发生的事情的概述,并指导游戏需要走向何方,而不会陷入游戏过程中运行的脚本细节。
在我们的当前设置中,当我们运行testLevel场景时,我们的GameManager对象位于GameManager脚本中,它设置了场景的相机和灯光,现在不再存在。
为了防止我们的GameManager游戏对象和脚本被清除,我们将添加一个单例设计模式,这样我们的GameManager脚本将始终存在于场景中。这种设计模式还将确保只有一个GameManager脚本(这也是这种设计模式得名的原因)。
在以下说明中,我们将扩展我们的原始GameManager代码,使其作为一个单例脚本工作。双击GameManager脚本,让我们开始吧:
-
在课程开始时,我们需要添加一个
static变量和一个public static属性,它们都指向我们的GameManager脚本:static GameManager instance; public static GameManager Instance { get { return instance; } }
我们这样做的原因是static意味着只有一个类型的游戏管理器。这正是我们想要的;我们不希望有相同管理器的多个实例。
- 接下来,我们需要在脚本开始时的
Awake函数中检查并分配我们的instance变量到GameManager类。
Awake函数以 Unity 函数DontDestroyOnLoad结束。这将确保包含我们的GameManager类的游戏对象在场景改变时不会被销毁。
小贴士
如果玩家死亡并失去所有生命,我们可以从当前所在的关卡场景移动到gameOver场景,但不会从场景中清除GameManager游戏对象,因为这个对象包含了运行游戏的主要核心方法。
-
添加一个
else循环以防止出现任何可能的重复GameManager游戏对象。我们可以在以下代码块中看到这两个步骤:void Awake() { if(instance == null) { instance = this; DontDestroyOnLoad(this); } else { Destroy(this.gameObject); } } -
为了使我们的代码更容易识别,我们将刚刚输入的代码包裹在
Awake函数中,并将其放入一个名为CheckGameManagerIsInTheScene的方法中。 -
从
Awake函数中调用该方法。提示
与
DontDestroyOnLoad类似的方法是MoveGameObjectToScene,它可以用来将单个游戏对象携带到另一个场景。这可以用于将玩家从一个场景移动到另一个场景:docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.MoveGameObjectToScene.html。
就这样,我们的单例设计模式就完成了!以下截图显示了我们的GameManager脚本应该看起来像什么:
![图 3.1 – 在我们的 GameManager 脚本中的单例代码模式]

图 3.1 – 在我们的 GameManager 脚本中的单例代码模式
- 最后,保存
GameManager脚本。
我们创建了一个单例设计模式,它不会在我们游戏中的场景之间切换时被清除,无论我们在哪个场景中,都能给我们提供全局控制游戏的能力。
现在,我们可以开始添加ScenesManager脚本并将其附加到与GameManager相同的游戏对象(在其检查器窗口中)。
设置我们的场景管理器脚本
通过创建另一个管理脚本,我们将从GameManager脚本中分担一些责任,使其与它持有的数据和方法的保持一致性。ScenesManager将接收和发送信息到GameManager。以下图表显示了当只与GameManager通信时,我们的ScenesManager脚本在框架中与GameManager的接近程度:
![图 3.2 – ScenesManager 在 Killer Wave UML 中的位置]

图 3.2 – ScenesManager 在 Killer Wave UML 中的位置
ScenesManager的目的,除了减轻GameManager的工作负担外,还包括处理与创建或更改场景相关的任何内容。这并不意味着我们只关注添加和删除游戏关卡;一个场景还可以包括启动标志、标题屏幕、菜单和游戏结束屏幕,所有这些都是ScenesManager脚本责任的一部分。
在本节中,我们将设置一个场景模板和两个方法。第一个方法将负责在玩家死亡时重置关卡(ResetScene());第二个方法将是游戏结束屏幕(GameOver())。
让我们以与第二章**, 添加和操作对象相同的方式开始,创建一个新的脚本。按照以下步骤操作:
-
将脚本命名为
ScenesManager。 -
将脚本添加到
GameManager游戏对象中。如果您需要有关将脚本添加到游戏对象的更多详细信息,请查看上一章的将我们的脚本添加到游戏对象部分。 -
从
GameManager和ScenesManager脚本中选择GameManager游戏对象,如图下所示:



让我们打开ScenesManager脚本并开始编码:
-
因为我们显然将要关闭和加载场景,所以我们需要在我们的
ScenesManager脚本中导入一个额外的库来支持这些操作:using UnityEngine.SceneManagement; using UnityEngine; -
在我们的脚本中,我们将有一个公共类,后面跟着通常继承的
MonoBehaviour:public class ScenesManager : MonoBehaviour {
现在,我们需要创建一个场景引用列表,正如之前提到的。我目前有以下场景标记:
-
bootUp:游戏信用 -
title:游戏名称,带有开始指令 -
shop:在开始游戏前购买升级 -
level1:第一级 -
level2:第二级 -
level3:最终级 -
gameOver:游戏结束—延迟直到返回标题场景
我们将把这些场景标记为枚举(在 C#语言中用enum表示)。这些值保持一致。
提示
如果您想了解更多关于枚举的信息,请查看docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/enum。
-
将以下代码输入到
ScenesManager脚本中:Scenes scenes; public enum Scenes { bootUp, title, shop, level1, level2, level3, gameOver }
我们将在本书后面的 Unity 编辑器中按顺序创建并添加这些场景。在我们这样做之前,让我们添加两个方法,从ResetScene()方法开始,这通常用于玩家死亡并且当前关卡重新加载时。另一个方法GameOver()通常在玩家失去所有生命或游戏完成时调用。
添加ResetScene()方法
当玩家失去一条生命但仍有剩余时,将会调用ResetScene()方法。在这个简短的方法中,我们将将其可访问性设置为public,并且它不返回任何内容(void)。
在这个方法中,我们将引用 Unity 的SceneManager脚本(不要与我们的ScenesManager类混淆),然后是 Unity 的LoadScene方法。我们现在需要提供一个参数来告诉LoadScene我们将加载哪个场景。
我们再次使用 Unity 的SceneManager脚本,但这次我们使用GetActiveScene().buildIndex,这基本上意味着获取场景的值编号。我们将这个场景编号发送到SceneManager以重新加载场景(LoadScene):
public void ResetScene()
{
SceneManager.LoadScene(SceneManager.GetActiveScene(). buildIndex);
}
这是一个小巧但有效的方法,可以在我们需要场景重置时随时调用。现在,让我们继续到GameOver()方法。
添加GameOver()方法
如您所预期的那样,这个方法在玩家失去所有生命并且游戏结束时被调用,这意味着我们需要将玩家移动到另一个场景。
在这个方法中,我们继续向ScenesManager脚本中添加内容:
public void GameOver()
{
SceneManager.LoadScene("gameOver");
}
}
与之前的方法类似,我们将此方法称为 public 并具有 void return 返回类型。在方法内部,我们调用相同的 Unity 函数 SceneManager.LoadScene,但这次我们调用 SceneManager Unity 函数,然后通过名称调用我们想要加载的场景(在这种情况下,gameOver)。
更多信息
SceneManager.LoadScene 还提供了一个 LoadSceneMode 函数,它为我们提供了使用两个属性之一的选择。默认情况下,第一个属性是 Single,它关闭所有场景并加载我们想要的场景。第二个属性是 Additive,它将下一个场景添加到当前场景旁边。这在交换场景时可能很有用,例如加载屏幕,或者保持前一个场景的设置。有关 LoadScene 的更多信息,请参阅 docs.unity3d.com/ScriptReference/SceneManagement.LoadSceneMode.html。
我们的 GameOver() 方法已经制作完成,并且当以与我们的 ResetScene() 方法相同的方式使用时,它可以全局调用。GameOver() 不仅可以在玩家失去所有生命时调用,也可以在用户完成游戏时调用。如果游戏意外崩溃,它也可以用作默认的重置,我们将进入 gameOver 场景。
接下来要添加到 ScenesManager 脚本中的方法是 BeginGame()。当我们需要开始玩游戏时,将调用此方法。
添加 BeginGame() 方法
在本节中,我们将向 ScenesManager 脚本添加 BeginGame() 方法,因为这将在我们访问 shop 场景后调用,我们将在 第五章**, 为我们的游戏创建商店场景 中介绍。
在上一节中仍然打开的 ScenesManager 脚本中,添加以下方法:
public void BeginGame()
{
SceneManager.LoadScene("testLevel");
}
我们刚刚输入的代码直接调用以运行 testLevel 场景,这是我们玩游戏的地方。然而,随着我们的游戏开始增长,我们将使用多个场景。
下一步是创建我们的场景并将它们添加到 Unity 构建菜单中,所以让我们继续这样做。记得在返回 Unity 编辑器之前保存 ScenesManager 脚本。
将场景添加到我们的构建设置窗口
我们的游戏将包含多个场景,玩家需要在这些场景中导航,然后才能通过关卡飞行他们的宇宙飞船。这将导致他们死亡或完成每个关卡和游戏,然后被带回到 title 场景。这也被称为游戏循环。让我们回到 Unity,在项目窗口中创建和添加我们的新场景。按照以下步骤操作:
-
前往我们在上一章开头创建的
Assets/Scene文件夹。 -
在 Scene 文件夹内部,在空白区域右键单击,以便出现下拉菜单,然后点击 Create,接着点击 Scene,如以下截图所示:

图 3.4 – 在 Unity 编辑器中创建一个空场景
-
将出现一个场景文件。将其重命名为
bootUp。 -
对
shop、level1、level2、level3、gameOver和title场景文件重复此过程。
一旦我们制作了所有场景,我们需要让 Unity 知道我们希望这些场景被识别并应用于项目构建顺序。这与我们在上一章中向构建设置窗口添加testLevel时的过程类似。要将其他场景应用到列表中,请执行以下操作:
-
从 Unity 编辑器的顶部,点击文件 | 构建设置。
-
列表中已经有
testLevel。如果没有,请不要担心,我们将在场景在构建列表中添加所有我们的场景。 -
从项目窗口,点击并拖动每个场景到构建设置 | 场景 在 构建开放空间中。
一旦我们添加了所有场景,按照以下顺序排列它们:
-
bootUp -
title -
shop -
testLevel -
level1 -
level2 -
level3 -
gameOver提示
注意,每个场景默认在其层次窗口中都有一个相机和一盏灯。这很好,我们将在本书的后面部分对其进行自定义。
构建设置窗口现在应该如下所示:

图 3.5 – 构建设置当前场景在构建列表中的顺序
我们将场景按此顺序放置的原因是为了使层级之间有一个逻辑上的递进。如图 3.4 中所示,您可以看到每个场景的右侧,场景是按增量计算的。因此,第一个要加载的场景将是bootUp场景。
现在我们已经为游戏添加了多个场景,我们可以考虑这样一个事实:我们可能不希望我们的GameManager方法中的相机和灯光设置方法在游戏的每个场景中运行。让我们简要回顾一下我们的GameManager脚本,并更新我们的LightSetup和CameraSetup方法,以及一些其他内容。
更新我们的GameManager脚本
在本节中,我们将回到GameManager脚本,并使其在控制我们的宇宙飞船时调用CameraSetup和LightSetup方法。
为了更新我们的GameManager脚本以支持各种场景的灯光和相机,我们需要执行以下操作:
-
在 Unity 编辑器中,从项目窗口导航到
Assets/Script。 -
在
GameManager脚本中,向下滚动到Start函数,并移除LightSetup();和CameraSetup();方法。 -
接下来,我们将在
GameManager脚本的顶部输入两个静态全局变量,与其它全局变量一起:public static int currentScene = 0; public static int gameLevelScene = 3; bool died = false; public bool Died { get {return died;} set {died = value;} }
currentScene是一个整数,它将保持当前场景的编号,我们将在以下方法中使用它。第二个变量gameLevelScene将保存我们玩的第一级,我们将在本章后面使用它。
-
仍然在
GameManager脚本中,创建一个Awake函数并输入以下代码:void Awake() { CheckGameManagerIsInTheScene(); currentScene = UnityEngine.SceneManagement.SceneManager. GetActiveScene().buildIndex; LightAndCameraSetup(currentScene); }
在我们刚刚输入的代码中,我们将 buildIndex 数字(上一节中从我们的 Build Settings 窗口中每个场景右侧的数字)存储在 currentScene 变量中。然后我们将 currentScene 的值发送到我们新的 LightandCameraSetup 方法。
-
我们需要添加到
GameManager脚本中的最后一部分代码是LightandCameraSetup方法,它接受一个整型参数:void LightAndCameraSetup(int sceneNumber) { switch (sceneNumber) { //testLevel, Level1, Level2, Level3 case 3 : case 4 :case 5: case 6: { LightSetup(); CameraSetup(); break; } } }
在我们刚刚编写的代码中,我们运行了一个 switch 语句来检查 sceneNumber 变量的值,如果它落入 3、4、5 或 6 的值,我们将运行 LightSetup 和 CameraSetup。
- 保存
GameManager脚本。
反思这一部分,我们创建了一个空场景的结构,每个场景都将在我们游戏中发挥其作用。我们还创建了一个 ScenesManager 脚本,当玩家获胜或死亡时,它将重置场景,或者移动到游戏结束场景。
现在我们已经设置了场景并构建了 ScenesManager 脚本的开始,我们可以专注于玩家的生命系统。
为玩家创建生命
在本节中,我们将使玩家拥有一定数量的生命。如果玩家与敌人碰撞,玩家将死亡,场景将重置,并从玩家那里扣除一条生命。当所有生命都耗尽时,我们将引入游戏结束场景。
在本节中,我们将使用以下脚本:
-
GameManager -
SceneManager -
Player
让我们先回顾一下 GameManager 脚本,并设置给予和扣除玩家生命的功能:
-
打开
GameManager脚本并输入以下代码:public static int playerLives = 3;
在脚本顶部,在输入类和继承之后,输入一个 static(意味着只有一个)整型变量 playerLives,并赋予其值 3。
接下来,我们需要为 GameManager 脚本创建一个新的方法,以确保玩家失去一条生命。在我们创建这个新方法之后,Player 脚本将在与敌人接触时调用它。
让我们继续我们的 GameManager 脚本。
-
要创建
LifeLost方法,在我们的GameManager类中输入以下代码:public void LifeLost() {
我们需要这个方法是一个 public 方法,以便可以从脚本外部访问。它被设置为 void,意味着该方法不返回任何内容,并且后面跟着方法名,括号内为空,因为它不接受任何参数。
-
因此,在
LifeLost()方法中,我们将使用以下代码的if语句检查玩家的生命值://lose life if (playerLives >= 1) { playerLives--; Debug.Log("Lives left: "+playerLives); GetComponent<ScenesManager>().ResetScene(); }
在审查了我们输入的if语句代码后,我们将通过添加注释来开始,以便让我们或其他开发者知道这个条件正在做什么(//lose life)。然后我们将添加if语句条件,检查玩家是否剩下至少一条生命。如果玩家确实还有一条或多条生命,我们将使用--运算符从玩家的生命值中减去 1,这仅仅是一种更快地说playerLives = playerLives - 1;的方式。
在扣除玩家生命值之后的代码行是不必要的,但它将在 Unity 编辑器的控制台窗口中通知我们,通过一个信息框告诉我们玩家剩余多少生命(用于调试目的),如下面的截图所示:

图 3.6 – 控制台窗口显示玩家剩余的生命数
在ScenesManager脚本中显示玩家剩余生命数之后,该脚本附加到GameManager游戏对象。我们可以使用GetComponent来访问ScenesManager脚本的ResetScene方法,这将重置我们的场景。
-
我们现在将进入
else条件,这表示玩家已经死亡:else { playerLives = 3; GetComponent<ScenesManager>().GameOver(); } }
如果我们的玩家没有剩余生命,这意味着if语句条件没有满足,因此我们可以提供一个else条件。在我们的else语句范围内,我们将玩家的生命值重置为3。
我们然后从ScenesManager类中访问GameOver()方法,这将带我们从当前场景跳转到gameOver场景。
最后,我们现在需要做的是让我们的Player脚本在玩家与敌人或敌人的子弹碰撞时调用LifeLost方法:
-
保存
GameManager脚本。 -
从
Player脚本(Assets/Script)开始。 -
滚动到其
Die方法。 -
从
Destroy(this.gameObject);这一行开始,输入以下代码:GameManager.Instance.LifeLost();
注意,我们可以直接通过使用GetComponent等代码调用GameManager脚本,而无需在场景中找到游戏对象来获取脚本。这是使用 Singleton 设计模式的强大之处,可以直接调用LifeLost方法。
-
保存
Player脚本。 -
在 Unity 编辑器中按Play键并碰撞敌人。
在testLevel中将等级重置为gameOver的消息。
以下截图显示了从testLevel到gameOver场景的转换:

图 3.7 – 玩家生命值减少和加载游戏结束场景
通过最少的代码,我们现在让我们的玩家拥有一定数量的生命。我们向游戏框架中引入了一个ScenesManager脚本,该脚本直接与GameManager通信,无论重启和更改场景。
作为旁注,您可能已经注意到,当我们切换到gameOver场景时,我们的GameManager游戏对象被带到了gameOver场景中。如果您还记得添加 Singleton 设计模式部分,我们设置了CheckGameManagerIsInTheScene方法,该方法在Awake函数中被调用。这意味着仅仅因为我们处于不同的场景,并不意味着Awake函数会被再次调用。
信息
记住,Awake函数只有在脚本激活时才会运行,并且只会运行一次,即使脚本附加到游戏对象并通过场景传递。
这是因为我们的gameOver场景只把GameManager游戏对象带到了gameOver场景中。它没有被激活,这意味着Awake函数没有被调用。
我们有我们基本的生命和场景结构,并且我们也使用了控制台窗口来帮助我们确认变化。
在我们继续之前,您可能会注意到,当玩家死亡时,场景中的灯光会变暗。以下截图展示了我的意思:

图 3.8 – 游戏中的灯光变暗
如您在前面的截图中所见,左边是我们开始时的场景,右边是玩家死亡时的场景。为了解决这个问题,我们只需要手动生成我们的灯光,而不是让 Unity 自动生成。
为了防止我们的灯光在场景之间变暗,我们需要做以下事情:
-
在 Unity 编辑器的顶部,点击窗口 | 灯光 | 设置。
-
灯光设置窗口将出现。在窗口底部,取消选中自动生成,然后点击旁边的按钮,生成灯光。以下截图供您参考:

图 3.9 – 未选中的自动生成框和生成灯光按钮
- 这将需要一点时间,因为 Unity 将设置新的灯光设置。一旦完成,保存 Unity 项目,应该就能解决这个问题。
注意,我们可能需要在本书后面的其他场景中手动设置灯光,例如其他关卡和shop场景。
让我们现在将注意力转向敌人,并添加一些功能,以便当它被玩家摧毁时,我们可以将分数添加到ScoreManager中,这是一个我们将要制作的新的脚本。
记分敌人击中
就像大多数游戏一样,我们需要一个计分系统来显示玩家在游戏结束时的表现。通常,在侧滚动射击游戏中,玩家每击杀一次都会得到奖励。如果我们转向我们的游戏框架图,我们可以看到ScoreManager与GameManager连接,就像ScenesManager一样:

图 3.10 – 杀手波 UML
我们添加评分系统的代码将再次是最小的。我们还想保持灵活性,以便不同的敌人有不同的得分。我们还希望,当我们向游戏中添加具有不同得分点的另一个敌人时,我们可以避免每次都更改我们的代码。
在本节中,我们将使用以下脚本:
-
EnemyWave -
ScoreManager -
ScenesManager -
SOActorModel
由于评分系统是我们游戏的一个关键因素,因此向 SOActorModel 添加一个简单的整数,将其常用值注入到我们的游戏对象中是有意义的。这种趋势将继续应用到其他脚本中。在我们引入 ScoreManager 之前,让我们先在我们的已制作脚本中添加一些代码。
准备 ScoreManager 脚本的代码
如果你还记得 第一章,设置和构建我们的项目,我们讨论了 SOLID 原则以及将代码添加而不是更改的重要性,否则我们可能会出错,我们的代码可能会开始变异,最终变得不再适合使用。为了准备,我们将向已经制作的脚本中添加代码,以便将 ScoreManager 脚本放置到位。让我们首先从 SOActorModel 开始。按照以下步骤操作:
-
从 项目 窗口中打开
SOActorModel脚本。 -
在
SOActorModel脚本中的变量列表中的任何位置,添加以下代码,该代码将用于包含敌人的分数:public int score; -
保存
SOActorModel脚本。
在我们向其他脚本添加更多代码以将 ScoreManager 放入游戏之前,我们需要承认我们已经更改了我们的 ScriptableObject 模板。
让我们在 Unity 编辑器中检查我们的 BasicWave Enemy 可脚本对象。按照以下步骤操作:
-
从
Assets/ScriptableObject文件夹。 -
在 BasicWave Enemy 上单击一次,你将看到 检查器 窗口有一个 Score 输入字段。
-
给
BasicWave Enemy赋予你选择的值。我给它赋值为200。只要它大于0,你赋予什么值都无关紧要。以下截图显示了带有更新后的 Score 值的 BasicWave Enemy 部分:

图 3.11 – 添加了敌人波分的属性和值
我们已经更新了 BasicWave Enemy 可脚本对象。现在我们需要关注 EnemyWave 脚本来创建和接收这个新变量。
-
打开
EnemyWave脚本。 -
在脚本顶部,我们有
health、travelSpeed和其他全局变量,向列表中添加一个额外的变量:int score;
我们现在需要更新 score 变量,从 ScriptableObject 值中获取。
-
在
EnemyWave脚本中,向下滚动直到找到ActorStats方法,然后添加以下额外的代码行:score = actorModel.score;
EnemyWave 脚本现在有一个 score 变量,其值由 SOActorModel 给定。我们需要做的最后一件事是在敌人因玩家的行动而死亡时将分数值发送到 ScoreManager。在我们这样做之前,让我们创建并编写我们的 ScoreManager 脚本。
设置我们的 ScoreManager 脚本
ScoreManager 脚本的目的是在玩家游戏过程中累计分数,直到他们到达 gameOver 场景。我们也可以给 ScoreManager 脚本添加其他与分数相关的功能,例如将我们的分数数据存储在玩游戏设备上,或者将分数数据发送到服务器以创建在线排行榜。现在,我们将保持简单,只收集玩家的分数。
我们可以创建并添加我们的 ScoreManager 脚本到游戏框架中,如下所示:
- 创建并附加一个名为
ScoreManager的脚本到GameManager游戏对象,类似于我们之前对ScenesManager所做的那样。
如果你不记得如何做这个,那么请查看本章的 设置我们的 ScenesManager 脚本 部分。以下截图显示了 ScoreManager 在 检查器 窗口中附加到 GameManager 游戏对象:
![Figure 3.12 – 将 ScoreManager 脚本添加到 GameManager 游戏对象]
![img/Figure_3.12_B18381.jpg]
Figure 3.12 – 将 ScoreManager 脚本添加到 GameManager 游戏对象
接下来,我们将打开 ScoreManager 脚本并添加代码来保存和发送分数数据。打开 ScoreManager 脚本并输入以下代码:
using UnityEngine;
如前所述,默认情况下,我们需要 UnityEngine 库。
-
继续通过检查并输入班级名称:
public class ScoreManager : MonoBehaviour {
这是一个公开的类,ScoreManager 继承自 MonoBehaviour 以增加脚本的函数性。
- 接下来,我们在脚本中添加我们的变量和属性。我们唯一关心的值是
playerScore,它是脚本私有的(因为我们不希望其他类访问它)。这个变量也被设置为static,这意味着我们不需要对这个变量的重复引用。
在此基础上,我们添加我们的 public 属性,它允许外部类访问 playerScore 变量。正如你将注意到的,PlayerScore 属性返回一个整数。在这个属性中,我们使用 get 访问器返回我们的私有 playerScore 整数。保持我们的变量私有是一个好习惯,否则你可能会将你的代码暴露给其他类,这可能导致错误。以下代码显示了如何完成此步骤:
static int playerScore;
public int PlayersScore
{
get
{
return playerScore;
}
}
访问器
要了解更多关于访问器的信息,请查看docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/get。
-
现在我们将转到
SetScore方法;它是一个公共方法,不返回任何值(void),SetScore名称接受一个名为incomingScore的整数参数。在此方法中,我们使用incomingScore将playerScore脚本(作为其总分)相加:public void SetScore(int incomingScore) { playerScore += incomingScore; } -
最后要添加的方法是
ResetScore方法。输入以下代码:public void ResetScore() { playerScore = 00000000; } }
我们可以在游戏开始或结束时调用此方法,以防止分数延续到下一场游戏。
- 保存脚本。
如前所述,我们现在可以回到EnemyWave脚本,将敌人的分数点值发送到ScoreManagers方法的SetScore,从而将其添加到玩家的总分数中:
-
从 Unity 函数
OnTriggerEnter打开EnemyWave脚本。 -
在标记为
if (health <= 0)的if语句的作用域内,在其作用域顶部输入以下代码行:GameManager.Instance.GetComponent<ScoreManager>().SetScore(score);
当这个特定的敌人因玩家而死时,此代码行将直接将敌人的score值发送到playerScore变量,并增加其总分数,直到玩家失去所有生命。
-
最后,为了确认分数是否正确累加,让我们像之前在
GameManager脚本的LifeLost方法中对playerLives整数所做的那样操作,并在控制台窗口中添加一个Debug.Log消息。 -
在
ScenesManager脚本下的GameOver()方法中,在其作用域顶部添加以下代码行:Debug.Log("ENDSCORE: " + GameManager.Instance.GetComponent<ScoreManager> ().PlayersScore);
此代码将告诉我们玩家得分多少,因为它直接访问ScoreManager并在游戏结束时获取PlayerScore属性。以下截图显示了总分的一个示例:
![Figure 3.13 – Game over score value displayed in the Console window]
![img/Figure_3.13_B18381.jpg]
图 3.13 – 在控制台窗口中显示的游戏结束分数值
- 最后,保存所有脚本。
在本节中,我们介绍了ScoreManager脚本及其基本工作结构,用于汇总我们的最终分数并在控制台窗口中显示最终计数。我们还向一些脚本添加了更多代码,而没有删除或更改它们的内容。接下来,我们将做一些不同的事情,这不需要任何编码,但会使我们更熟悉 Unity 的声音组件。
为玩家的子弹创建声音
到目前为止,我们的游戏一直是静音的,但声音是任何游戏中的重要因素。在本节中,我们将介绍我们的第一个声音组件。我们将从为玩家开火时创建声音效果开始。
如果您愿意,可以添加您自己的子弹声音类型。您可以通过以下方式为玩家的标准子弹添加声音:
-
在 Unity 编辑器中,导航到
Resources文件夹。将新文件夹命名为Sound。 -
将项目面板中的
Player_Bullet预制体拖放到层次面板中。 -
在选择
Player_Bullet的情况下,点击添加组件按钮在检查器面板中。 -
在下拉菜单中开始键入(并选择)
音频源。 -
将
PlayerLaser.mp3文件拖放到选中的Player_Bullet中。左下角的音频文件需要拖放到右侧的音频源组件中:
![图 3.14 – 在检查器窗口中将音频文件添加到 Player_Bullet 的音频源游戏对象]
检查器窗口

图 3.14 – 在检查器窗口中将音频文件添加到 Player_Bullet 的音频源游戏对象
-
当
Player_Bullet被实例化时,声音将播放。 -
如果音量太高,只需在检查器窗口的音频源组件中将其降低。
信息
除了音频源组件中的音量选项外,还有音调来改变子弹的声音,以及立体声平衡来使声音在左或右扬声器中更加突出。最后,因为这是一个二维游戏,我们不希望声音受到我们的摄像头与子弹距离的影响。因此,我们将空间混合切换滑到最左边,以确保它不会受到其距离的影响。
-
最后,点击
Player_Bullet预制体,并在层次结构窗口中移除子弹。 -
播放场景并开始射击。你会听到激光声,在场景视图中,你会看到现在附加到玩家子弹的扬声器符号。
这就结束了关于音频的简短部分,但在这本书中我们还会更多地涉及音频。别忘了,如果你在任何地方遇到困难,请检查本章的Complete文件夹,比较场景和代码,以确保没有遗漏。
摘要
在本章中,我们通过扩展GameManager脚本来扩展和加强我们的游戏框架结构,通过扩展其代码。这意味着无论场景如何变化,它都不会被删除。我们还引入了得分和场景管理器,这些最初都计划在我们的游戏框架中。这两个额外的管理器从游戏管理器那里接管了责任,并为你的游戏添加了额外的功能。我们确保这些脚本不会破坏我们的原始代码(删除、溢出或补偿我们的游戏管理器)。现在你的游戏有一个工作得分系统,以及多个可以重新启动和更改的场景,而且代码非常少。我们还引入了声音,我们将在后面的章节中更详细地实现。
在下一章中,我们将减少对代码密集型内容的关注,而是关注游戏的艺术。尽管我们是程序员,但我们需要了解如何使用 Unity 的 API 操作资源以及如何进行动画。只需一点点的编码,这将使我们能够理解编辑器和我们的脚本之间的联系。我们还将涉及一些粒子效果。
干得好——你已经做了很多。在我们继续之前,尝试以下问题。这些问题类似于你将在程序员考试中遇到的问题。
模拟测试
这是您的第一次迷你模拟测试。这些测试代表了您的最终 Unity 考试的各个部分。这次迷你模拟测试只包含五个问题。稍后在这本书中,我们将介绍包含更多问题的更多迷你模拟测试。
幸运的是,您将只会被测试到我们迄今为止所覆盖的内容:
-
您被要求开发一款恐怖生存游戏,其中玩家依赖口袋手电筒。以下是您迄今为止编写的代码:
void Start() { Light playersTorch = GetComponent<Light>(); playersTorch.lightMapBakeType = LightMapBakeType. Mixed; playersTorch.type = LightType.Area; playersTorch.shadows = LightShadows.Soft; playersTorch.range = 5f; }
您会注意到,玩家的手电筒没有投射任何光线或阴影。您应该更改哪些代码才能使其按预期工作?
-
将
playersTorch.lightBakeType设置为LightmapBakeType.Realtime。 -
将
playersTorch.range设置为10。 -
将
playersTorch.shadows设置为LightShadows.Hard。 -
将
playersTorch.type设置为LightType.Point。 -
您已经开始创建您的第一款独立游戏,超级摩托车赛车 64。您已经编写了代码以使操纵杆工作,并开始测试您的摩托车在弯道上的表现。您已经注意到,在绕过第一个弯道后,即使您已经松开了操纵杆,摩托车仍然继续转弯。
您检查了您的代码和操纵杆,它们似乎都运行良好,这表明问题出在输入管理器上。
您应该在输入管理器中做出什么更改?
-
增加重力。
-
将
Snap设置为true。 -
增加
Deadzone。 -
减少
Sensitivity。 -
您已经开始使用纸笔制作游戏框架的模板。您已经绘制了几个将导致创建单个
GameManager脚本的经理脚本。您只需要一个GameManager脚本,它将始终存在于您的场景中。
哪种设计模式适合在持久实例角色中使用GameManager脚本?
-
原型
-
抽象工厂
-
单例
-
构建者
-
您已被要求创建一个侧滚动游戏的原型,其中玩家向敌人投掷石头。游戏运行良好,摄像头从左向右移动,直到关卡结束。要投掷石头,您的代码实例化一个石头的预制体,然后给予一个力(
Rigidbody.AddForce)以使石头飞出,从而产生石头被投掷的错觉。
您的主开发人员表示,您的方法在内存性能上花费太多,并希望您使用设计模式在石头数组中存储最多 10 块石头。一旦石头被使用,而不是被销毁,它应该返回到数组中。
开发人员指的是哪种设计模式?
-
抽象工厂
-
对象池
-
依赖注入
-
构建者
这就是你的第一次迷你模拟测试的结束。要检查你的答案,请参考本书后面的附录部分。你做得怎么样?为了复习任何错误的答案,我建议你翻回到最后几章的相关部分,并在需要的地方刷新你的记忆。遗憾的是,考试可能有点像记忆力游戏。每个人的记忆力都不同,而且大多数通过这些考试的人在通过之前都曾在某些部分失败过。
无论哪种方式,你完成这些测试越多,你在这些测试上的能力就会越强。只需保持专注,你就能顺利通过!
第四章:第四章: 应用艺术、动画和粒子
在本章中,我们将将几个艺术效果应用到玩家飞船和我们在第二章**中导入的场景上,即添加和管理对象*。我们将使用几个围绕玩家飞船包裹的地图,为它增添科幻主题,包括一些漂亮的粒子效果,我们将添加到我们的霓虹蓝喷气式飞机上。我们还将引入一个太空背景,它也将由粒子效果构建。然后,您将亲自动手设置自己的 Unity 动画控制器,我们可以用它来操纵我们在场景中创建的粒子,以产生玩家飞船以光速穿越太空的印象,然后在敌人进攻前减速。最后,我们将在我们的脚本中为敌人应用一些动画。
本章的大部分内容是关于熟悉 Unity 作为编辑器能做什么,以及我们在编辑器中学到的绝大多数内容也可以通过代码实现。这就是为什么作为一个程序员,了解我们可以在项目中操作的内容非常重要。
简而言之,我们将涵盖以下主题:
-
为玩家飞船添加视觉效果
-
创建粒子效果
-
导入并动画化场景
-
使用脚本动画三维敌人
那么,让我们开始改变玩家飞船的外观吧。
本章涵盖的核心考试技能
我们将探讨编程核心交互:
- 实现和配置游戏对象行为和物理
我们还将探讨在艺术管道中工作:
-
理解材质、纹理和着色器,并编写与 Unity 渲染 API 交互的脚本
-
理解光照并编写与 Unity 光照 API 交互的脚本
-
理解二维和三维动画,并编写与 Unity 动画 API 交互的脚本
-
理解粒子系统和效果,并编写与 Unity 粒子系统 API 交互的脚本
我们将涵盖场景和环境设计的编程:
- 识别实现游戏对象实例化、销毁和管理的方法
最后,我们将涵盖在专业软件开发团队中工作:
- 识别构建脚本以实现模块化、可读性和可重用性的技术
技术要求
本章的项目内容可以在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_04找到。
您可以下载每个章节的项目文件完整版,链接为github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition。
本章的所有内容都包含在章节的unitypackage文件中,包括一个Complete文件夹,其中包含我们在本章中将要完成的所有工作。
查看以下视频,了解代码的实际应用:bit.ly/3OHV4xi。
为玩家飞船预制件添加视觉效果
在本节中,我们将专注于玩家飞船。我们将创建一系列不同的视觉艺术技巧,使我们的飞船看起来具有未来感,而无需物理改变其几何形状。我们将为我们的飞船创建并应用一种材料,这种材料用作外壳来存储和显示多个地图。这些地图负责针对玩家飞船上的特定通道。因为这本书是专门为程序员编写的,所以我创建了几张这样的地图,您可以将其拖放到位于检查器窗口中的材料组件中。
通常情况下,如果一个三维模型,如玩家飞船,被应用了纹理,该模型需要经过一种称为展开的方法。展开就像剥去模型的表面并将其平铺开来,以便可以应用纹理。然后,未剥去的表面被重新包裹在三维模型周围。如果我们不先展开模型就应用纹理,飞船的纹理将会混乱,因为它不知道应该在何处正确显示纹理。我们不需要深入探讨展开的细节,因为这超出了本书的范围,但请记住,玩家的飞船模型必须进行展开。
以下截图显示了玩家飞船的三维模型在左侧,以及其展开版本,右侧带有纹理:
![图 4.1 – 玩家飞船 3D 模型及其 UV 图]
![img/Figure_4.01_B18381.jpg]
图 4.1 – 玩家飞船 3D 模型及其 UV 图
我们还将向飞船投射彩色光线,但只允许其某些部分发出光线,并确保光线不会照到任何其他使用 Unity 层系统的游戏对象。我们将涵盖 Unity 的另一个重要部分是粒子系统;我们将创建自己的粒子喷气式推进器,它将从飞船的尾部动画。
以下截图显示了玩家飞船当前的外观,位于左侧。在本节结束时,我们将拥有一个具有动画喷气式推进器的科幻外观飞船,显示在截图的右侧:
![图 4.2 – 在本章中,玩家飞船将被纹理化,并携带自己的推进器粒子效果]
![img/Figure_4.02_B18381.jpg]
图 4.2 – 在本章中,玩家飞船将被纹理化,并携带自己的推进器粒子效果
现在我们继续创建一个可以用来存储本章unitypackage下载中地图的材料。
为玩家飞船预制件创建材料
目前,我们的玩家飞船已经应用了一个默认材质,我们无法在 Unity 编辑器中编辑它。为了能够更改飞船的颜色并应用多个贴图,我们首先需要创建一个材质,然后将其应用到玩家的飞船上。为此,请按照以下步骤操作:
-
在
Assets/Material/Player中右键单击空白区域。 -
在顶部单击创建。
-
然后,单击材质。
-
将出现一个材质图标,以蓝色突出显示。将此材质重命名为
PlayerShip。小贴士
要在不选择材质的情况下重命名材质,请双击图标下方的文本以将蓝色突出显示恢复。然后,输入一个名称——在我们的例子中,是
PlayerShip。对于 Mac,在选定的材质上按Enter键并开始键入其名称以重命名它。
以下截图显示了材质的创建过程:

图 4.3 – 在 Unity 编辑器中创建材质
有两种方法可以将材质应用到飞船上。第一种也是最简单的方法是将材质拖放到场景视图中的PlayerShip模型上。
小贴士
可以通过Renderer.material属性在脚本中创建和更新材质。查看docs.unity3d.com/ScriptReference/Renderer-material.html以获取更多信息。
第二种方法——可能也是更好的方法,因为它是一种更受控的更新材质的方法——是在其预制文件夹中选择PlayerShip。然后,在检查器窗口中执行以下操作:
-
在网格渲染器组件旁边是材质下拉箭头。单击箭头,使其指向下方。
-
在组件内部需要关注的两个主要点是以下内容:
-
1。 -
Default-Material。
-
-
要么点击
Default-Material(或 whatever the material is called)右侧的小按钮,如图所示,要么将我们刚刚创建的PlayerShip材质拖放到与Default-Material相同的位置。
以下截图显示了位于检查器窗口中的网格渲染器组件的位置:

图 4.4 – player_ship Mesh Renderer 位置在检查器窗口中
完成后,PlayerShip、1和位于PlayerShip底部(或 whatever you named the material)的Default-Material。
小贴士
Default-Material不能被编辑,因为它通常与新的网格渲染器游戏对象共享。
现在,我们需要更新飞船的预制件(在 第一章,设置和构建我们的项目)中提到的预制件。如果 PlayerShip 模型仍然被选中,转到 Prefab 文件夹中的 PlayerShip,这将是必要的,因为我们已经直接更新了预制件。在下一节中,我们将分解我们现在可以应用于材质的各种地图。
将地图应用于我们的玩家飞船材质
我们为 PlayerShip 预制件创建的新材质现在可以容纳各种地图。我们的材质将为每个地图留出空槽;这些地图将为玩家的飞船添加细节,从颜色到假细节,如切割、凹痕和凹槽,这些细节并未物理建模到玩家的飞船中。我们还可以强调玩家飞船将吸收光的地方。
这里是我们将应用于 PlayerShip 预制件的一些地图选择:
playerShip_diff): 反照率图包含图像的颜色,类似于漫反射图,但没有光和阴影。以下截图显示了我们的反照率图:

图 4.5 – 玩家飞船反照率图
playerShip_met): 金属图专注于表面的反射性和光。以下截图显示了我们的金属图:

图 4.6 – 玩家飞船金属图
playerShip_em): 发射图不接受光线,非常适合发光效果(自发光)。以下截图显示了我们的发射图:

图 4.7 – 玩家飞船发射图
playerShip_nrm): 法线图存储每个像素的方向。此图的通用用途是保持高分辨率细节,给网格带来更多多边形的错觉。以下截图显示了我们的法线图:

图 4.8 – 玩家飞船法线图
playerShip_oc): 遮挡图提供有关模型哪些区域接收光的信息。以下截图显示了我们的遮挡图:

图 4.9 – 玩家飞船遮挡图
现在,我们将通过以下步骤将这些地图应用于 PlayerShip 模型:
-
选择
player_ship游戏对象,该对象可以在Assets/Prefab/Player文件路径位置中选择)。 -
为了在将我们的地图应用于
player_ship材料槽时更容易操作,锁定位于 检查器 窗口顶部的player_ship选择,如图所示(确保在完成拖放文件后解锁 检查器 窗口):

图 4.10 – 锁定检查器窗口
- 在
Texture文件夹中,将地图拖放到PlayerShip材料组件槽中。
在我们将文件拖放到指定的框中之前,请确保您的法线和发射贴图已正确设置。让我们从法线贴图开始。
当涉及到应用法线贴图时,有一些额外的步骤需要处理。首先,Unity 可能不会识别 Normal Map 作为法线贴图文件。当我们将法线贴图文件拖放到 Material 组件中的槽位,如图中所示,Inspector 窗口下的 Normal Map 槽位会出现一个信息框。这个信息框包含一条消息(This texture is not marked as a normal map)和一个 Fix Now 按钮。点击此按钮,以便正确配置法线贴图。以下截图显示了信息框的外观:

图 4.11 – 修复法线贴图
另一种修复法线贴图问题的方法是执行以下操作:
-
在 Project 窗口中选择法线贴图文件。
-
然后,在 Inspector 窗口中,我们有一个显示法线贴图的 Import Settings 选项的面板。
-
在选项区域的顶部,点击 Texture Type 旁边的下拉菜单,确保它被选为 Normal Map。
-
最后,点击 Inspector 窗口右下角的 Apply 按钮。
以下截图显示了从 Texture Type 下拉菜单中选择法线贴图文件,并在下拉菜单中选择 Normal map:

图 4.12 – 将纹理类型设置为法线贴图
将地图拖放到 Material 组件时,另一个潜在问题是需要检查 Emission 槽旁边的框,才能接受地图。以下截图突出显示了 Emission 槽,并显示了需要检查的框:

图 4.13 – Emission 检查框已勾选
很好,现在我们知道了可能的问题,我们可以将文件拖放到指定的框中。
导航到 Assets/Texture 文件夹,将每个文件拖放到正确的槽位中:

图 4.14 – 将地图拖放到玩家飞船材质的正确槽位
更多信息
材质属性,如发射颜色,可以通过使用 SetColor 属性通过脚本创建和修改。有关更改材质颜色或发射颜色的更多信息,请参阅 docs.unity3d.com/ScriptReference/Material.SetColor.html。
一旦我们将所有地图拖放到指定的槽位,我们的 player_ship 模型应该看起来不同,因为它现在具有金属光泽。然而,我们还没有完成。接下来,我们需要在飞船上添加一些霓虹灯。
向我们的PlayerShip预制体添加霓虹灯
我们目前的船看起来像金属,稍微有点暗淡,上面有一些科幻风格的图案。这不是艺术考试,我们的初步任务不是让这艘船看起来很棒,而是要了解我们添加到它上面的地图和效果。如前一小节简要提到的,我们可以在船上添加一些光线,这些光线也会对船的地图做出反应。以下截图显示了应用所有地图后我们的船目前的样子;你的可能很闪亮,但这并不重要:

图 4.15 – 应用所有地图的`Player Ship
接下来,我们将使船的部分区域以科幻霓虹蓝光点亮,结合点光源和发射图。
要在我们的船上添加灯光,我们需要做以下几步:
-
从
Assets/Prefab/Player。 -
选择
player_ship并将其拖动到层次结构窗口。 -
在下拉菜单中点击
Light。 -
当你在层次结构窗口中看到
player_ship时,将鼠标悬停在场景窗口中,然后按键盘上的F键来放大。
这个点光源将作为船周围的发光效果,只影响玩家的船和发射图,通过 Unity 的层系统。但首先,我们将关注灯光组件在检查器窗口中的设置:

图 4.16 – player_ship的灯光组件属性值
从层次结构窗口中选择点光源,我们可以在检查器窗口中更改我们的新灯光设置:
player_ship还将有一个黄色的小工具围绕它,以表示光的大小,如下面的截图所示:

图 4.17 – 围绕player_ship的光线小工具
提示
小工具是一个在场景窗口中出现的指示器,你在游戏窗口中是看不到的。小工具作为指导显示,以显示某物的位置和/或比例。
-
50): 范围会增加/减少黄色球体的大小,使光线向外扩展更多。我觉得设置为50已经足够覆盖整个船了。 -
0080FFFF。(这将设置红色、绿色、蓝色和 alpha 设置。) -
10): 光的强度。 -
0): 光线弹射到其他物体上。对于点光源,不支持实时间接弹射阴影。 -
渲染模式(重要):确保灯光始终开启,不会因为性能下降而关闭。
-
PlayerShip): 我们将在下一节讨论这个问题。我们使用蓝色光(以我的情况为例)给船上的地图添加霓虹灯效果。我们理想情况下不希望光线扩散到其他资产上,如果它们靠近玩家的船。重要提示
在光组件上玩玩;不需要它与我的颜色或强度完全相同。
一旦这些设置(除了剔除掩码)在我们的检查器窗口中更新,我们的飞船应该在各个区域点亮霓虹灯。在下面的截图中,我在玩家飞船模型后面放置了八个球体。注意现在我们的新霓虹灯光与球体发生了冲突。我将在下一部分解释我们如何解决灯光与游戏中的其他对象冲突的问题:

Figure 4.18 – 玩家飞船的光线与近 3D 对象的冲突
通过使用剔除掩码可以解决这个问题,因为我们可以使蓝色光线仅通过具有特定层掩码的玩家飞船显示。
要创建一个新层,我们需要进入标签 & 层部分,这可以通过两种方式访问:
-
第一种方法是点击屏幕右上角工具栏中的层选项卡。会出现一个包含可用层的下拉菜单。点击底部选项,编辑层...。
-
第二种方法是在层次结构窗口中选择任何游戏对象,然后点击层旁边的选项卡。然后,点击底部选项,添加层...:

Figure 4.19 – 添加新层
31因为它有编辑器的内部用途。
- 层
8到30可以使用。我将把PlayerShip输入到用户层 8字段中,如下面的截图所示:

Figure 4.20 – 添加名为 PlayerShip 的层
- 现在,在场景中或
Prefab文件夹内点击player_shipGameObject,并将它的点光源剔除掩码选项从所有内容更改为PlayerShip:

Figure 4.21 – PlayerShip 层在下拉列表中
-
然后,从层次结构中选择
player_ship模型。 -
在
player_shipPlayerShip中。因为我们添加了一个player_ship模型。 -
点击是,更改子对象,如下面的截图所示。这将把点光源层更改为PlayerShip:

Figure 4.22 – 确认更改所有子游戏对象
提示
一旦你将某物放入player_ship预制件,它将始终由PlayerSpawner实例化。
最终结果给我们一个酷炫的、霓虹般闪亮的蓝色飞船,它不会影响周围的任何游戏对象,如下面的截图所示(您的可能与这个不同):

Figure 4.23 – 我们的玩家飞船现在有霓虹发射灯
现在,让我们继续为玩家飞船添加粒子。
向我们的 PlayerShip 预制件添加粒子
在本节中,我们将创建一个粒子效果,给飞船的推进器带来运动的错觉。粒子系统本身分为不同的组件类别,这将影响粒子的行为。我们将关注放置、方向和粒子效果的寿命周期,这是一项可以转移到其他效果(如火焰、烟雾、水泄漏等)的技能。以下截图显示了我们的玩家飞船带有动画的粒子效果,这是我们接下来要创建的:
![Figure 4.24 – 带有粒子效果的玩家飞船]
![Figure_4.24_B18381.jpg]
Figure 4.24 – 带有粒子效果的玩家飞船
因此,让我们创建一个空的游戏对象来包含粒子系统:
-
在层次窗口的空白区域右键单击。
-
从出现的下拉菜单中选择创建空对象。
-
将空游戏对象命名为类似
playerJet的名称。 -
我们希望这个效果跟随玩家的飞船,所以将
player_ship对象拖放到playerJet上,放置在player_ship游戏对象上。 -
最后,我们需要将
playerJet移动到我们的player_ship对象后面,直到粒子开始发射的位置。我将我的移动到其0.5。
以下截图显示了粒子开始的位置和playerJet变换设置:
![Figure 4.25 – playerJet 游戏对象位于玩家飞船后面]
![Figure_4.25_B18381.jpg]
Figure 4.25 – playerJet 游戏对象位于玩家飞船后面
这样,我们的空游戏对象就创建好了,并且放置在玩家飞船模型后面。现在,我们可以在下一节中将粒子效果添加到空游戏对象中。
创建粒子效果
在本节中,我们将从上一节中的空游戏对象开始创建我们的粒子效果;类似于我们之前几节所做的那样,我们需要将所有粒子系统设置为playerJet游戏对象的子对象:
-
在
playerJet中。 -
从下拉菜单中选择效果,然后选择粒子效果。
-
重命名
thruster。
我们现在应该看到一个粒子系统,它以默认的粒子喷雾直接指向我们,如下面的截图所示:
![Figure 4.26 – 默认粒子系统]
![Figure_4.26_B18381.jpg]
Figure 4.26 – 默认粒子系统
接下来,我们需要调整粒子系统的大小并旋转到正确的方向,确保它以正确的方向喷射。
在检查器窗口中,选择我们的thruster对象,将其变换组件设置更改为以下内容:
-
0,0,和0 -
0,90,和0 -
0.3,0.3,和0.3
有时,我们的粒子系统对象可能不会更新,或者在我们更改或撤销其设置时,它可能会在场景窗口中消失。
要尝试在场景窗口中重启粒子系统使其激活或动画化,请在层次窗口中选择粒子系统。您将在场景视图的右下角注意到一个粒子效果弹出窗口。然后,执行以下步骤:
-
点击停止以停止粒子系统对象的发射。
-
点击重启。
以下截图显示了位于场景窗口右下角的粒子效果菜单:

图 4.27 – 粒子效果菜单
希望如此,如果粒子系统之前未激活,现在应该是激活的。如果它仍然没有激活,请尝试在层次窗口中选择不同的游戏对象,然后回到粒子系统并再次重复停止/启动方法。
在层次窗口中仍然选中我们的thruster粒子系统后,在检查器窗口中点击下拉按钮,如下截图所示:

图 4.28 – 粒子系统属性下拉按钮
现在,我们在检查器窗口中看到了一个可能看起来令人不知所措的选项列表,但我们只会更改几个选项,以使我们的粒子系统达到预期的效果。大多数 Unity 属性都有它们自己的工具提示选项。如果您不知道这些是什么,请选择 Unity 编辑器的顶部栏,将鼠标悬停在粒子系统属性上。几秒钟后,将出现一个描述这些属性的说明。
我们需要更改的粒子系统选项如下:
0.05:

图 4.29 – 持续时间属性
-
预加热:当播放时,预加热的系统将处于仿佛已经发射了一个循环周期的状态。只有当系统循环时才能使用。此选项应勾选。
-
0。此选项应设置为0.5。 -
0。 -
3D 起始大小:这是粒子的起始大小。
-
1,2。
要获取更多字段的选项,请执行以下操作:
-
点击选项右侧的下箭头。
-
从下拉菜单中,点击在两个常量之间随机,如下截图所示:

图 4.30 – 选择在两个常量之间随机
信息
代价最低的曲线是常量,因为它只需要一个值。
-
3D 起始旋转:如果启用,我们可以分别对每个轴进行旋转控制。此选项应勾选。要获取更多字段的选项,请执行以下操作:
-
点击选项右侧的下箭头。
-
从下拉菜单中,点击在两个常量之间随机并输入以下矢量旋转:
-

-
00FFD5B7。 -
4。
以下截图提供了应设置的设置参考:

图 4.31 – 整体粒子系统属性值
我们在这里更改了很多设置。总结一下,我们创建了一群出现后很快就会消失的粒子。如果它们存在时间过长,我们的推进器对象就会穿越屏幕(你可能希望也可能不希望这样)。我们消除了它的方向,因为我们将在稍后将其改为一个将粒子推向大致方向的力,从而形成一个不太可预测的模式。
小贴士
记住,屏幕上同时显示的粒子越多,场景的需求就越高。为了尽可能保持流畅,我们让粒子保持它们所需的时间(即,它们有较短的寿命),并且我们将每个粒子的大小保持尽可能小,而不是很大。
以下截图显示了我们现在闪烁的粒子系统:

图 4.32 – 当前粒子系统状态
现在我们继续在检查器窗口中浏览粒子系统设置。
设置粒子系统的发射部分
在本节中,我们将控制粒子系统的粒子速率;在每个标题下,我将显示一个信息框,显示该部分的提示描述。你可以通过将鼠标移至部分名称上查看提示信息。这同样适用于检查器窗口中的某些值。以下截图显示了提示信息的示例:

图 4.33 – 发射提示信息
thruster粒子系统的下一个子设置是50。
如前所述,如果有疑问,可以将设置调整到最大或最小,以查看是否有任何即时的视觉反馈来了解属性的作用。你总是可以撤销设置。作为额外的预防措施,在更改任何设置之前,你总是可以保存你的工作:

图 4.34 – 发射速率随时间设置为 50
在这里我们并没有做任何激进的事情,只是稍微减少了粒子数量。在本章的后面部分,出于性能考虑,可能需要进一步降低变量,具体取决于这款游戏要移植到哪个平台。
在下一节中,我们将设置粒子如何进入场景。
设置粒子系统的形状部分
在我们粒子系统设置的下一部分,我们可以更改形状设置及其属性。
信息
形状的提示信息描述是发射体积的形状,它控制了粒子的发射位置及其初始方向。
在形状部分,我们将调整粒子发出的起始点。我们将更改的设置如下:
-
形状:定义了粒子可以从中发射的体积形状以及起始速度的方向。将此选项设置为球体。
-
0.02.
我们需要关注的只是粒子发出的点。以下是需要设置的设置截图:

图 4.35 – 更新形状为球体及其半径为 0.02
我们玩家的飞船推进器现在显示更多的集中光:

图 4.36 – 我们玩家的飞船光现在更集中
如前所述,我们在第一个部分就停止了粒子系统的方向,在下一部分,我们将使用Force over Lifetime来大致指示粒子将去哪里。
设置粒子系统的Force over Lifetime部分
在这个简短的部分,我们将改变我们想要粒子去的地方的力。回顾之前的截图,我们可以看到我们的飞船有一个光,现在只需要稍微推回一点,以产生旅行的错觉。
信息
Force over Lifetime提示描述是控制粒子在其生命周期内的力。
与其他属性不同,这个属性需要通过选择其名称左侧的复选框来打开。
一旦激活,我们唯一需要调整的设置是设置10,如下面的截图所示:

图 4.37 – 生命周期内力值更新
小贴士
如前所述,在设置上要勇于尝试。意外和突破极限可以创造出可能对游戏其他部分和未来项目有用的新效果。
因此,现在我们的粒子看起来被拉伸了,类似于推进器,如下面的截图所示:

图 4.38 – 粒子系统现在略微拉伸
现在我们来改变纹理,看看我们是否可以为粒子添加更多细节。
设置粒子系统的Renderer部分
Renderer部分控制每个粒子的视觉效果。这是我们应用自己的材质的地方。在材质中有一个自定义纹理。
信息
Renderer提示描述是指定粒子如何渲染。
我们将更新Particle System材质,使其显示精灵图集,我们将在下一节中使用它进行动画。
信息
什么是精灵图集?它是一系列图像,通常以网格形式排列,用于动画。
将thruster材质文件拖放到Renderer部分的Material字段中,位置在Assets/Material文件路径,如下面的截图所示。
player_ship 对象现在分配了新的 thruster 粒子系统材质,看起来是点状的。在下面的截图中,thruster 材质的缩略图被拖放到 渲染器 部分的 材质 字段中:

图 4.39 – 将推进器材质拖放到渲染器/材质槽中
在应用了材质和纹理之后,我们现在在船尾发光的地方看到了很多点。我们已经做对了所有的事情,但是由于这个纹理表现得像动画,我们需要更新其 纹理图集动画 设置。
设置粒子系统的纹理图集动画部分
创建粒子效果的最后一步是让 Unity 正确地动画化精灵图集。在我们设置这个之前,让我们先看看我们输入到粒子系统中的纹理。
信息
纹理图集动画 工具提示描述是 粒子 UV 动画。这允许我们指定一个纹理图集(具有多个瓦片/子帧的纹理)并按每个粒子进行动画或随机化。
以下截图来自我们的 thruster 材质:

图 4.40 – 精灵图集
上一张截图包含一个 8 x 8 网格中的 64 张图片。这个纹理不需要额外的设置。如果有多余的图片数量,我们就必须使用 Unity 的精灵编辑器逐个裁剪每张图片,这可能会变得很繁琐。
更多信息
更多关于精灵编辑器的信息可以在 docs.unity3d.com/Manual/SpriteEditor.html 找到。
幸运的是,我们不需要担心做这件事。让我们更仔细地看看材质。
在以下截图中,我们可以看到 着色器 选项(截图顶部)设置为 粒子/添加 类别(理想的发光效果着色器),这是用于粒子系统中最常用的着色器之一。
在 Assets/Material 文件位置,我们有我们的 thruster 材质文件。点击文件会在 检查器 窗口中显示其属性:

图 4.41 – 推进器材质属性
我们可以通过改变其 着色 亮度值来改变粒子的强度,就像之前的截图所示。请随意进行自己的更改。我将保持我的设置不变。
回到 8 和 8。
信息
如您所回忆的,在上一个部分中,我们的粒子看起来是点状的。这是因为我们在一个粒子中显示了纹理图集中的所有 64 张图片。有了纹理动画图集,我们将这 64 张图片分成了单个图片,这些图片将动画化到每个粒子上。
以下截图显示了带有 纹理表动画 设置的粒子系统的延续:

图 4.42 – 更新纹理表动画瓦片值
这就是我们的最终结果:

图 4.43 – 我们玩家的推进器
如果你对自己的最终结果满意,在保存我们的 player_ship 预制件之前,我们需要采取最后一步。因为我们已经在 PlayerSpawner 脚本中改变了 player_ship 的比例,所以我们也需要对我们的 thruster 游戏对象做同样的操作。
要更改 thruster 游戏对象的 localScale 设置,我们需要执行以下操作:
-
在
Assets/Script。 -
双击
PlayerSpawner脚本,并向下滚动到以下代码行:playerShip.transform.localScale = new Vector3(60,60,60); -
在上一行代码下方,添加以下代码以调整
thruster游戏对象的大小:playerShip.GetComponentInChildren<ParticleSystem> ().transform.localScale = new Vector3(25,25,25);
上述代码访问玩家的船的 ParticleSystem 组件,并将其在所有轴上的比例缩放设置为 25。
-
保存
PlayerSpawner脚本。 -
在 Unity 编辑器中,选择 层次 窗口中的
player_ship对象,然后在 检查器 窗口中点击 覆盖 | 应用全部 按钮。
如前所述,对粒子系统要大胆;如果你不熟悉它们,请使用工具提示,并尝试调整设置——你很快就会习惯的。你可以复制并粘贴 thruster 游戏对象,并更改其颜色、排放、力、比例等。混合不同的元素以创建不同类型的推进器。
这是我用七个粒子系统制作的一个例子:

图 4.44 – 添加到玩家船上的各种粒子
粒子系统 也可以通过脚本进行操作,这就是为什么作为 Unity 程序员,我们熟悉属性,但并不擅长掌握技术。在 Unity 程序员考试中,你可能会被问到粒子系统的属性以及粒子系统在创建效果时有什么好处。尽管我们没有在本节中涵盖所有属性,但了解每个属性的作用是很好的实践——例如,了解 Size over LifeTime 属性只是随着时间的推移缩小粒子。
我们使用的粒子系统属性之一是 Texture Sheet Animation,我们提供了一个预先制作的纹理表来分割我们的单个图像以创建动画。
信息
当创建粒子系统时,它会生成一个可预测的模式。这被称为程序模式;这种模式的优点是 Unity 知道粒子在过去和未来的位置。当例如相机远离粒子系统时,这也帮助提高了性能;然后,它可以被裁剪。然而,如果粒子系统被修改,例如将其模拟空间更改为世界空间,粒子系统将变得不可预测和非程序化,这将使其无法提高性能。
想了解更多关于程序模式的信息,请查看以下链接:blogs.unity3d.com/2016/12/20/unitytips-particlesystem-performance-culling/。
在下一节中,我们还将使用粒子系统,但这次是为了背景创建星星,使其看起来在我们身边飞驰。我们还将使用 Unity 的动画控制器以不同的速度动画化星星。
导入和动画化背景
在本节中,我们将熟悉 Unity 的动画控制器。我们将通过在关卡开始时创建一个快速移动的星星和粒子背景(是的,没错,更多的粒子)来让我们的玩家飞船以光速(好吧,至少给人一种这样的印象)移动,然后当前方出现敌人时,我们将放慢一切。
在我们以“光速”开始动画之前,我们需要准备层次结构窗口:
-
在层次结构窗口中,在空白区域右键单击。
-
从下拉菜单中选择创建空。
-
点击新游戏对象并将其重命名为
GameSpeed。 -
再次这样做,并将第二个游戏对象命名为
_SceneAssets。 -
将
GameSpeed拖放到_SceneAssets游戏对象上。 -
确保两个游戏对象的变换属性值都设置为重置。
-
最后,将
_SceneAssets游戏对象从Assets/Prefab项目窗口拖动。小贴士
记住,除了定期保存场景和项目外,我们还需要确保我们创建的预制件将位于场景中并经常使用,以存储游戏对象及其组件的设置。
这是已经设置好的 层次结构 窗口,准备添加一些额外的游戏对象到我们的场景中。观察我们如何为游戏创建一个活跃的动画场景,我们可以用两种方法来处理侧滚动射击游戏。一种方法是有一个大型的静态背景,移动玩家和摄像机通过关卡,对抗敌人。另一种方法是保持摄像机静止,当我们在设定的时间触发敌人进入场景时,让背景移动或动画通过摄像机。为什么我们会选择第二种方法?因为当我们制作游戏时,作为一个程序员,我们需要关注对我们来说重要的事情——在这种情况下,玩家是最重要的。此外,玩家被限制在屏幕比例内。可以说,开发并调试一个移动的固定比例摄像机会更尴尬,迫使玩家穿越一个包含其他游戏对象的场景。我们还可以考虑物理因素,如碎片和更多游戏对象之间的碰撞,这可能会引起潜在的问题。作为一个程序员,我发现寻找最简单的选项总是最好的。
话虽如此,让我们继续制作我们的背景:
-
在 层次结构 窗口中右键点击
GameSpeed。 -
从下拉菜单中选择 创建空对象。
-
点击新的游戏对象并将其重命名为
ScreenBackground。
ScreenBackground 游戏对象将容纳星星粒子系统。我们将带入场景的粒子系统是预先制作的;我认为没有必要继续制作更多粒子,因为我们已经为玩家的飞船制作了一个。
从 Assets/Particle warpStars_pe 预制件到 层次结构 窗口中的 GameSpeed 游戏对象,使其成为子对象。
在本节中我们做出的更改,层次结构 窗口的内容应该类似于以下截图:
![图 4.45 – 当前层次结构布局
![img/Figure_4.45_B18381.jpg]
图 4.45 – 当前层次结构布局
我们已经更新了 层次结构 窗口,为这一章添加了第二个粒子系统。这改善了结构并增加了清晰度,因为我们的游戏开始扩展到更多功能。现在让我们继续关注粒子系统及其在场景中的放置:
-
在
warpStars_pe中如果没有被选中。 -
将注意力转移到 检查器 窗口中,将其 变换 设置调整为以下内容:![img/Table_02.jpg]
在我们的粒子系统设置在正确的位置后,我们现在可以专注于为游戏背景添加另一层,这将是一个大四边形上的 ScreenBackground 纹理。
让我们继续向 ScreenBackground 游戏对象添加更多功能:
-
在
ScreenBackground。 -
在 检查器 窗口中,点击 添加组件 按钮。
-
从出现的下拉菜单中,开始输入
网格过滤器直到它出现在可点击的列表中。
以下截图显示了 ScreenBackground,配备了三维 四边形 多边形网格:

图 4.46 – 从网格下拉菜单中选择四边形网格过滤器
使用创建的 四边形 网格,我们需要通过 网格渲染器 组件使其在我们的 场景 和 游戏 窗口中可见。
-
在 层次结构 窗口中选择
ScreenBackground游戏对象,并在 检查器 窗口中,点击 添加组件 按钮。 -
在下拉菜单中,开始输入
Mesh Renderer,直到你在列表中看到它,然后点击它。
与我们对 player_ship 预制件和之前的粒子系统所做的一样,我们需要为 ScreenBackground 对象创建并应用一个材料:
-
在
Assets/Material文件位置的空白区域右键单击。 -
从下拉菜单中选择 创建。
-
然后,点击 材料。
-
在选择了新的材料后,将其重命名为
backGround_Wallpaper。
因为我们的 ScreenBackground 游戏对象现在有一个 MeshRenderer 对象,并且我们已经为它创建了一个材料,我们现在需要应用它:
-
在
ScreenBackground。 -
将
backGround_Wallpaper材料从 项目 窗口拖放到 网格渲染器 部分,放入 材料 子部分的 元素 0 插槽中,如下截图所示:

图 4.47 – 将 backGround_wallpaper 材料拖放到元素 0
让我们回顾一下到目前为止在本节中完成的工作。我们已经将游戏对象放置在 backGround_Wallpaper 的正确位置。
现在,我们将设置材料以使其变得相当基础。它不需要很多复杂的着色器,所以一个简单的低资源移动着色器就足够了。
信息
着色器通常是数学脚本,告诉我们的材料其图形和光照如何表现。
确保在 层次结构 窗口中仍然选择了 ScreenBackground。
在 检查器 窗口中,向下滚动到 材料 组件:
-
如果 材料 组件没有展开,请点击白色球体旁边的箭头。
-
当 材料 组件展开时,我们可以看到许多我们不需要的映射,所以让我们将着色器更改为更基础的类型。
-
点击 着色器 字段的选择下拉菜单。通常,标准 将是默认着色器。
-
从下拉菜单中,点击 移动。
-
然后,点击 漫反射。
以下截图显示了此过程:

图 4.48 – 为我们的材料选择移动漫反射着色器
这将我们的 材料 属性简化到最小需求,如下截图所示:

图 4.49 – backGround_wallpaper 材料当前状态
在这个 材质 组件中,我们真正关心的是我们将要提供给它的 纹理 和其 偏移量 值。
信息
什么是偏移量?偏移量 是我们的纹理在 UV 图上应用的位置。例如,如果我们增加 偏移量 属性的 X 位置,应用到材质上的纹理将重叠并出现在四边形的另一侧。
我们现在将继续处理我们的背景 ScreenBackground 纹理:
-
在仍然位于 检查器 窗口的 材质 组件中,右上角有一个大正方形,称为 无(纹理)。点击 选择(参考前面的截图)。
-
出现了一个下拉菜单。开始输入
spaceBackground,直到出现选项,然后点击它。
我们应该有一个名为 ScreenBackground 的四边形,其颜色为黑色,上面有白色的小点,如下面的截图所示:

图 4.50 – 我们的 quad spaceBackground
在我们开始动画这个纹理之前,我们需要做与我们为粒子系统所做的一样,更新其 ScreenBackground 以覆盖摄像机的视锥角,并在 变换 更新后显示我们的图像。
-
在
ScreenBackground游戏对象中。 -
在 检查器 窗口中,使用以下设置更新其 变换 组件:

下面的截图显示了我们的当前场景视图:
-
我们有一个非常大的四边形,上面有空间纹理。
-
我们有一个白色的网格状相机视图。
-
玩家的飞船位于摄像机和四边形之间。
-
右下角显示了用户看到的最终结果:

图 4.51 – 我们当前的游戏场景环境设置
我们已经更新了我们的 层次结构 窗口以包含两个背景层。第一层显示通过星星,第二层是我们从本章下载文件中添加的第二个粒子系统。第二层是一个包含纹理的四边形游戏对象。现在让我们继续为我们的背景和空间扭曲粒子创建动画控制器。
添加动画控制器
使用动画控制器是一种控制动画状态的方法。我们的玩家飞船将以光速行驶几秒钟,然后在我们玩家被敌人攻击之前,我们将放慢速度。
以下截图中的左侧飞船比右侧的飞船有更多的拖尾粒子。星系背景在左侧也比在右侧移动得快(在这些静态截图中你几乎看不到):

图 4.52 – 下一步将动画背景中的星星
因此,让我们创建并将 Animator 附加到ScreenBackground对象和粒子系统的父对象上。
小贴士
使用 Animator Controller,如果您同时动画化多个游戏对象,请确保您的 Animator Controller 是这些游戏对象的父对象。不能有子对象动画化其父对象(父对象指的是Hierarchy窗口中对象上方的游戏对象)。
查看GameSpeed游戏对象。如信息框中所述,Animator Controller 会动画化所有子对象,但它不能动画化父对象。因此,让我们添加 Animator Controller:
-
在
GameSpeed游戏对象中。 -
在Inspector窗口中,点击Add Component按钮。
-
在下拉菜单中,开始输入
Animator,直到它出现,然后点击它。
以下截图显示了GameSpeed游戏对象:
![Figure 4.53 – Select the Animator from the dropdown
![img/Figure_4.53_B18381.jpg]
图 4.53 – 从下拉菜单中选择 Animator
现在我们有了GameSpeed游戏对象。接下来要做的就是创建并附加 Animator Controller 到Controller字段。以下截图显示了Animator组件的设置:
![Figure 4.54 – The Animator Controller component
![img/Figure_4.54_B18381.jpg]
图 4.54 – The Animator Controller component
在进行之前,我们需要创建一个Animator文件夹。在Assets文件夹中创建一个空文件夹。命名为Animator。
进入Animator文件夹,继续创建 Animator Controller:
-
在Project窗口的空白区域右键点击。
-
点击Create。
-
点击Animator Controller(参考以下截图的左侧)。
-
重命名新的
GameSpeed_Controller。 -
最后,将此 Animator Controller 拖放到其Inspector窗口中的
GameSpeedAnimator Controller 上(参考以下截图的右侧)。
以下截图显示了 Animator Controller 的创建以及如何将其应用到Animator组件中:
![Figure 4.55 – Drag and drop the GameSpeed_Controller into the Animator Controller slot
![img/Figure_4.55_B18381.jpg]
图 4.55 – 将 GameSpeed_Controller 拖放到 Animator Controller 槽中
在本节中,我们创建并应用了GameSpeed游戏对象。在下一节中,我们将查看 Animator Controller 中的动画状态。
在 Animator Controller 中创建状态
在本节中,我们将使用 Animator Controller 创建一个用于快速动画背景场景和粒子的状态;接着是第二个状态,将背景和粒子减速以表示玩家飞船以较慢的速度行驶(这也有助于使我们的游戏不那么分散注意力)。让我们创建第一个状态。
要创建一个状态,请按照以下说明操作:
-
双击放置在
GameSpeedAnimator组件中的GameSpeed_Controller对象。 -
动画器窗口将打开并显示一些默认状态:进入、任何状态和退出。
-
在动画器窗口内的空白区域右键单击。
-
将会出现一个下拉菜单。点击创建状态。
-
然后,点击空:

图 4.56 – 创建一个空动画状态
如您所猜,我们刚刚创建了一个空状态。
-
重复此过程以创建第二个状态。
更多信息
Unity 的动画控制器还提供了与我们的动画的分层。例如,我们可以为一个可以跑步、跳跃和射击的玩家进行动画处理。我们可能希望同时播放几个这样的动画,我们可以通过层(参见上一张截图的左上角)来实现。我们可以改变每个动画的影响,或权重,在 Unity 中被称为,并且我们可以使用覆盖(其他层的将忽略信息)或叠加(添加到另一个动画之上)设置来在动画之间混合。
如果您想了解更多关于动画层的信息,请访问
docs.unity3d.com/Manual/AnimationLayers.html。
一旦我们创建了第二个状态,让我们做一些整理:
- 点击并拖动退出和任何状态到一边。我们不会使用这些。进入将自动附加到我们创建的第一个状态。
现在让我们重命名我们的状态。
-
点击名为New State的橙色状态。
-
在检查器窗口中,点击右上角标有新建状态的位置。
-
删除此状态并将其重命名为
BackGround_Intro_Speed。 -
在键盘上按Enter键以确保保存名称。如果您点击离开,有时它不会保存更改。
-
现在,将我们创建的另一个状态重命名为
New State 0,将其重命名为BackGround_InGame_Speed。小贴士
您可以使用鼠标滚轮放大和缩小动画器窗口。
要放大,滚动鼠标滚轮向上。
要缩小,滚动鼠标滚轮向下。
要平移,按住鼠标中键。
不要担心状态的精确位置;这更多是一个外观问题。我们只需要有一个BackGround_Intro_Speed状态,一个BackGround_InGame_Speed状态靠近它。
以下截图显示了我们应该关注的三个状态:

图 4.57 – 动画控制器状态
这三个动画状态最终将连接到每个状态上的一些线条;这些线条允许创建一个条件(例如if语句;更多信息请参见 docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/if-else)以便一个状态可以移动到另一个状态。
在我们查看这个之前,我们还需要意识到每个状态可以以不同的速度运行。我们将调整状态的速度,使其与它们所包含的动画速度相匹配。要更改我们状态中的动画速度,请执行以下操作:
-
在
BackGround_InGame_Speed状态中。 -
在其
1到0.1。
另一个状态将保持不变。
当场景开始时,第一个状态是 BackGround_Intro_Speed,然后一旦该动画连接到它(我们目前还没有这样做),接下来将播放 BackGround_InGame_Speed。我们需要连接最后一个状态,以便它可以在之后播放。
要连接一个状态,请执行以下操作:
-
在
BackGround_Intro_Speed状态中。 -
从下拉菜单中,点击 Make Transition。
-
然后,点击
BackGround_InGame_Speed。
我们现在应该有一个状态连接到另一个状态。
在本节中,我们更深入地探讨了 Animator Controller,创建了我们的开场和游戏动画状态。我们设置了状态的速度,最后,连接了过渡线,以便我们知道动画状态流。所有这些额外游戏对象、Animator Controller 和状态的结构意味着我们现在已经到了可以开始动画化场景的阶段。
动画
最后,我们实际上要动画化一些东西。我们只会覆盖一个基本的动画,但它将帮助我们理解动画设置,这将支持我们在考试和未来的项目中。
因此,让我们直接进入并动画化背景以及我们的背景粒子:
-
在
Assets/Animator文件夹位置。 -
在空白区域右键单击,并从下拉菜单中选择 Create。
-
然后,点击 Animation。
-
将新动画命名为
BackGround_InGame_Speed。 -
重复此过程,并将新动画命名为
BackGround_Intro_Speed。
以下截图显示了创建 Animation 文件的过程:

图 4.58 – 在 Unity 编辑器中创建动画文件
开场动画将播放一次,因为它将是一阵星星的爆发,然后第二个动画将循环播放,连续播放以产生永不结束的星星和粒子星星穿过 Game 窗口的错觉。
话虽如此,在 BackGround_InGame_Speed 动画文件中,并在 Inspector 窗口中,勾选 Loop Time 旁边的框。
我们现在需要在 Animator 窗口中将我们的两个 Animation 文件应用到它们的动画状态中。
要连接这些新的 Animation 文件,请执行以下操作:
-
在
GameSpeed游戏对象中。 -
双击
GameSpeed_Controller。 -
Animator 窗口打开。在动画控制器中选中我们创建的两个动画状态之一。
-
将我们刚刚创建的 Animation 文件拖放到 Inspector 选项卡的 Motion 字段中(参见图示)。
-
选择我们创建的另一个状态,并使用另一个匹配的 Animation 文件重复拖放过程。
我们现在有两个 Animator Controller 状态,应用了空动画剪辑。
以下截图显示我们的BackGround_Intro_Speed动画文件被拖放到动画状态 | 运动字段中:
![图 4.59 – 将 BackGround_Intro_Speed 文件拖放到运动槽中]
![图 4.59 – 图 4.59_B18381.jpg]
图 4.59 – 将 BackGround_Intro_Speed 文件拖放到运动槽中
更多信息
您还可以在 Animator Controller 中创建混合树。混合树专门用于将一系列动画混合成一种形式。在混合树中,有不同的类型:1D,一系列2D,和直接。
混合树可以用来将动画从行走改为跑步(1D)或用于更复杂的动画,如面部表情(直接)。
要了解更多关于混合树的信息,请查看docs.unity3d.com/Manual/class-BlendTree.html。
让我们专注于我们的动画文件,并开始对场景进行动画制作。
首先,我们需要打开动画窗口:
-
在 Unity 编辑器窗口顶部,点击窗口。
-
然后,点击动画,或者可以使用Ctrl(或在 macOS 上为command)+ 6 快捷键。
-
接下来,回到我们的
BackGround_Intro_Speed动画文件(这应该位于Assets/Animator文件夹结构中)。这将更新窗口内的动画名称(请参考以下截图):
![图 4.60 – 动画窗口,已选择 BackGround_Intro_Speed 动画]
![图 4.60 – 图 4.60_B18381.jpg]
图 4.60 – 动画窗口,已选择 BackGround_Intro_Speed 动画
小贴士
如同 Unity 中的大多数窗口,我们可以锁定窗口,使其不更新到另一个游戏对象或,在这种情况下,动画。
要锁定窗口,点击动画窗口右上角的锁形符号。
在此阶段,锁定动画可能是个好主意,因为我们将在层次结构和检查器窗口中点击不同的游戏对象。
我们首先将动画spaceBackground纹理:
-
保持
GameSpeed游戏对象在层次结构中选中。如果我们选择其他对象,我们将在动画窗口中失去动画功能。 -
在动画窗口中,点击圆形红色录音按钮(位于动画文件名上方)。注意动画窗口部分变红,这告诉我们我们处于录音模式。
-
然后,在
ScreenBackground。 -
现在,在
backGround_Wallpaper材质设置中。 -
同时,确保我们的动画白色指示线完全在左侧,如图所示。
-
接下来,将
0改为-10。注意字段变红,因为在动画窗口中有提示。 -
现在,点击并按住动画窗口中的白色线,将其移动到右侧,使其不位于刚刚制作的动画上方:

图 4.61 – 动画背景壁纸材质属性的偏移值
- 将
-10更改为1。注意,在以下截图中的300(5 分钟):

图 4.62 – 添加第二个关键帧并更改背景壁纸偏移值
- 在我们的两个动画点之间前后移动白色线条(动画师称此为scrub)。注意四边形上的星星是如何移动的。
现在,让我们对warpStars_pe粒子系统做类似的事情:
-
确保动画窗口仍然锁定并记录。
-
将动画指示线完全移回到左侧,到另一个动画关键帧的开始处。
-
从
warpStars_pe。
在检查器标签页的变换部分,进行以下更改:

然后,将白色线条移动到与星形背景关键帧完全相同的点。
小贴士
我们可以在动画窗口中点击下一帧按钮跳转到下一个关键帧(位于播放按钮右侧的按钮,而不是编辑器播放按钮)。
在层次结构窗口中仍然选择warpStars_pe游戏对象,并在检查器窗口中更新其变换设置,如下所示:

-
在动画窗口中尝试前后拖动,以查看在场景视图中看起来如何。你应该看到粒子从右向左移动。
-
在动画窗口中,关闭记录设置。
这样一个动画就完成了,还有一个要完成。下一个过程与我们已经做过的类似,但稍微快一点。
在层次结构窗口中仍然选择warpStarts_pe,并且动画窗口仍然打开,执行以下操作:
- 按照以下截图所示,使用鼠标单击并拖动选择所有关键帧:

图 4.63 – 选择所有关键帧
-
释放鼠标并按Ctrl(或在 macOS 上为Command)+ C复制关键帧。
-
现在,通过单击当前动画的名称并选择另一个,切换到
BackGround_InGame_Speed动画,如图所示:

图 4.64 – 在动画窗口中选择我们的第二个动画文件,BackGround_InGame_Speed
-
注意名称如何更改以反映我们所在的动画。
-
现在,在图表区域单击并使用Ctrl(或在 macOS 上为command)+ V键盘命令。
-
现在,我们应该已经将之前的动画粘贴到这个动画中。我们可以在窗口内操作结果,如图所示:
小贴士
如果您在动画窗口中看不到所有关键帧,请选择窗口内的一个开放区域,并在键盘上按下F键。这将自动调整所有关键帧。

图 4.65 – BackGround_InGame_Speed 动画文件及其在动画窗口中粘贴的关键帧
最后,我们可以操作关键帧:
-
点击动画窗口以开始录制。
-
从
ScreenBackground。 -
从
backGround_Wallpaper-10到1。 -
点击
1到2。 -
接下来,我们在最左侧的
warpStars_pe关键帧中修改warpStars_pe动画,并在键盘上按下Delete键。现在,将最后一个关键帧从末尾移动到开头。
在停止录制之前,我们需要停止动画的渐出(在动画末尾减速)。
要使我们的backGround_Wallpaper设置在固定的动画速度上,我们需要做以下操作:
-
在动画窗口中,点击并拖动以选择所有键。
-
右键单击,从下拉菜单中选择两个切线 | 线性。
-
停止录制。
让我们回顾一下到目前为止我们所做的工作。我们取了1,使其回到其X值偏移的起始位置。
在第一个动画中,我们将粒子从左到右移动;在第二个动画中,我们保持粒子在右侧,以避免场景过于杂乱,并显示我们并没有移动得那么快。
我们现在处于动画的最后步骤;其余工作在 Animator Controller 中进行。从 Animator Controller 中,我们可以声明需要循环的内容以及我们的动画如何相互关联。
在本章的最后,让我们访问动画控制器,并开始从一种状态到另一种状态的拼接:
-
从
GameSpeed游戏对象。 -
然后,在
GameSpeed_Controller。 -
现在,点击
BackGround_Intro_Speed和BackGround_InGame_Speed之间的过渡线(以下参考截图所示)。
关于动画之间的过渡,以下截图通过两个蓝色条设置这些状态示例。选择以下设置:
- 具有退出时间:已勾选。
具有退出时间提示信息显示过渡具有固定的退出时间。
0.1
退出时间提示信息显示退出时间是当前状态的标准时间。
- 固定持续时间:已勾选。
固定持续时间提示信息显示过渡持续时间与状态长度无关。
2.5
过渡持续时间(秒)提示信息显示过渡持续时间为秒。
0.1
过渡偏移提示信息显示下一个状态的标准开始时间。
- 中断源:无
中断源提示信息显示可以被以下过渡中断:

图 4.66 – 在检查器窗口中更新过渡值
这个过渡的图形比较粗糙。对于动画来说,输入完美的图形也很不自然。我建议从场景中移除任何敌人。点击播放按钮并调整图上的选择条。每次选择改变时,动画都会再次播放。注意 Animator Controller;你会看到一个进度条开始和结束。这将有助于动画拼接的时间安排。
信息
动画过渡可以帮助将一个动画平滑地过渡到另一个。例如,如果我们想在精确的时间框架内将一个动画移动到另一个,我们会关注固定持续时间和过渡持续时间参数(如前一张截图所示)。
关于动画过渡的更多信息,请查看docs.unity3d.com/Manual/class-Transition.html。
我得到的结果非常平滑且效果良好,但我建议忘记图形。将编辑器置于播放模式,并拖动选择条直到得到适合你的拼接点。
那就是 Animator Controller 设置的结束。这是那种解释起来需要很长时间但一旦知道了怎么做就非常快的事情。
你可能迫不及待地想要回到编码,因为我们主要一直在使用 Unity 的编辑器工具。那么,让我们回到 IDE,开始看看如何进行动画制作。
动画我们的三维敌人
这里有一个非常简单、快速的动画脚本,用于你的敌人。目前,敌人只是以波浪模式上下移动。然而,单位本身是静态的。
让我们用一些代码给我们的敌人增加一点额外的生命力:
-
在
Assets/Prefab/Enemies。 -
双击
enemy_wave预制体,并在层次结构窗口中选择enemy_wave_ring。 -
在检查器窗口中,点击添加组件按钮。
-
在下拉菜单底部点击新建脚本。
-
将新的 C#脚本命名为
BasicEnemyRotate。 -
然后,输入以下代码:
using UnityEngine; public class BasicEnemyRotate : MonoBehaviour { [SerializeField] float speed = 0; void Update () { transform.Rotate(Vector3.left*Time.deltaTime*speed); } }
这是一个微小的脚本,用于动画化我们的敌人部分。有两件事需要仔细查看:
-
变量是一个名为
speed的私有浮点数,具有SerializeField属性,因此可以在检查器窗口中看到。更多关于此属性的信息可以在docs.unity3d.com/ScriptReference/SerializeField.html找到。 -
在我们的
Update函数中,我们根据设定的速度随时间旋转游戏对象。我在检查器中将敌人的旋转速度设置为200。
一旦你通过enemy_wave预制体创建了、添加并更新了你的脚本新内容。
- 要更新预制体,请点击层次结构窗口左上角的左箭头,如图下所示:

图 4.67:更新预制内容
-
将出现一个弹出窗口以确认新的更改,只需点击保存。
-
现在你已经知道如何更新预制体,再次从项目窗口中选择
enemy_wave预制体,像之前一样双击它。
接下来,我们将按照以下步骤更新其视觉效果:
-
在
enemy_wave_core中展开enemy_wave游戏对象。 -
在检查器窗口下,选择元素 0参数右侧的小型圆形遥控按钮。
-
选择
basicEnemyShip_Inner材质。 -
从层次结构窗口中选择
enemy_wave_ring。 -
从
basicEnemyShip_Outer。 -
最后,像之前一样更新预制体,通过点击层次结构窗口左上角的箭头,然后在弹出的窗口中点击保存。
-
在编辑器顶部点击播放,我们现在应该看到我们的敌人正在旋转并着色:

图 4.67 – 正在向玩家移动的旋转敌人
最后,将所有新脚本移动到Script文件夹中。
之后,我们可以根据玩家的技能水平加快敌人的旋转速度,使其看起来更具侵略性。
这是一章很长的内容,但我们涵盖了粒子和动画,这些对于考试来说很重要。通过更多的实践和理解,我们所学的益处将真正开始显现。现在是熟悉这两种技能的好时机,因为它们通常被忽视。正是这些技能让你与众不同。
摘要
在本章中,我们进入了艺术世界。我们让玩家的飞船栩栩如生,给它配备了一系列地图和灯光。然后,我们转向 Unity 的粒子系统,创建了一个带有扩展选项的推进器对象。接着,我们进入了动画制作,动手制作场景背景和动画粒子扭曲星。我们讨论了状态和转换,然后通过为敌人编写一些动画代码来平息一切。
这已经很多了!如果你再次回顾这一章,你会更快地完成它,因为你会发现,如果你还没有这样做,你可以复制和粘贴动画关键帧,复制和粘贴粒子系统,并对其进行调整。
在下一章中,我们将查看一个新的场景,在该场景开始之前,我们将通过引入商店来升级玩家的飞船。我们还将介绍流行的免费游玩游戏概念,这在移动游戏中很常见,游戏可以免费下载,用户可以选择通过观看广告来获得游戏内积分。
干得好!你所学的所有内容都将有助于你的考试和未来的项目。
第五章:第五章:为我们的游戏创建商店场景
在本章中,我们将整合并扩展在上一章中大量帮助制作玩家和敌舰的可脚本化对象。我们将定制一个新的商店场景,其中我们将使用可脚本化对象为玩家的飞船添加新的升级。
我们还将探讨射线投射的常见用途;如果你不熟悉它们,它们最好被描述为从一个点到另一个点的无形激光:

图 5.1 – 使用射线投射识别游戏对象
当射线击中具有碰撞器的游戏对象时,它可以检索有关该对象的信息,然后我们可以进一步操作我们击中的对象。例如,我们可以向一个立方体游戏对象投射射线,射线将确认它是一个立方体。因为我们有立方体的引用,我们可以改变它的颜色或缩放,或位置或销毁它 – 我们几乎可以对其做任何我们想做的事情。在这里,我们将使用这个射线投射系统,在我们点击或触摸屏幕时,从摄像机的位置向三维空间中的按钮发射一个点。
在本章中,我们将涵盖以下主题:
-
介绍我们的商店脚本
-
定制我们的商店选择
-
使用射线投射选择游戏对象
-
向我们的描述面板添加信息
到本章结束时,我们将能够轻松地将一件艺术品添加功能,使其更有用。脚本化对象是我们的朋友,在本章中我们将更多地使用它们 – 添加属性和值,以便我们的商店中的每个选项都可以持有精灵、价格和描述。商店按钮将是 3D 的,并通过我们的无形激光(射线投射系统)进行选择。
本章涵盖的核心考试技能
我们将涵盖编程核心交互:
-
实现和配置游戏对象的行为和物理
-
实现和配置输入和控制
我们还将涵盖在艺术管道中工作:
- 理解材质、纹理和着色器,并编写与 Unity 渲染 API 交互的脚本
本章还涵盖开发应用程序系统:
-
解释用于应用程序界面流程的脚本,如菜单系统、UI 导航和应用设置
-
解释用户自定义脚本,如角色创建器、库存、店面和内购
-
分析用于用户进度功能的脚本,如得分、等级和游戏内经济,利用 Unity Analytics 和 PlayerPrefs 等技术
-
分析用于二维叠加的脚本,如抬头显示(HUDs)、小地图和广告
我们还将涵盖编程场景和环境设计:
- 介绍实现游戏对象实例化、销毁和管理的方法
最后,我们将涵盖在专业软件开发团队中工作:
- 识别用于结构化脚本的模块化、可读性和可重用性技术
技术要求
本章的项目内容可在 github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_05 找到。
您可以下载每个章节的项目文件的全部内容,在 github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition。
本章的所有内容都存储在章节的 unitypackage 文件中,包括一个 Complete 文件夹,其中包含我们在本章中将要完成的所有工作。
查看以下视频以查看 代码的实际应用:bit.ly/3klZZ9q。
介绍我们的商店脚本
在本节中,我们将创建一些新的可脚本化对象,就像我们创建玩家的飞船设置(健康、速度、火力等)时做的那样。您可以参考 第二章 的 介绍我们的脚本化对象 (SOActorModel) 部分,以提醒如何完成此操作。我们不会改变敌人的或玩家的飞船,而是将操纵商店的选择(带有选择网格)以添加玩家可以选择的自己的飞船升级。这些升级然后将转移到玩家的飞船上,这将从视觉上得到识别,并且其中两个升级将对游戏玩法进行修改。
在我们进一步详细介绍之前,让我们回顾一下商店脚本在我们介绍的 第一章,设置和构建我们的项目 中的游戏框架中的位置。
以下图表显示了商店脚本的位置:
![Figure 5.2 – Killer Wave's UML
![img/Figure_5.02_B18381.jpg]
图 5.2 – 杀手波浪的 UML
我们的三种商店脚本(PlayerShipBuild、ShopPiece 和 SOShopSelection)从 PlayerShipBuild 连接到主中心 GameManager 脚本相互连接。简而言之,每个脚本在商店场景中的职责如下:
-
PlayerShipBuild是商店的整体功能,包括广告和在游戏中的信用控制。此脚本可以分解为更多脚本,但为了尽量保持我们的框架最小化,对于演示来说是可以的。 -
ShopPiece处理玩家飞船升级选择的内 容。 -
SOShopSelection是一个可脚本化的对象,它包含将在我们的商店场景中的每个选择网格中使用的数据类型。
让我们看看我们将要创建的场景,并开始将其应用于商店脚本,如下所示:
-
从
Scene文件夹。 -
双击
shop场景。 -
从
Assets/Prefab中拖放ShopManager预制体。 -
选择
0、0和0。 -
如果你想有不同的背景颜色,在层次结构窗口中仍然选择摄像机,将清除标志属性从天空盒更改为纯色。
信息
确保摄像机保持与我们之前在第二章中设置的相同屏幕比例(即 1920 x 1080)。以下截图供参考。
以下场景分为四个部分:

图 5.3 – 我们的商店布局
看一下之前的截图,让我们逐一过一下编号的点:
-
从
testLevel场景开始(这是我们之前四章一直在工作的场景)。 -
在左上角(2),我们有玩家飞船的视觉表示。我们可以看到如果他们购买升级,它将看起来像我们的玩家飞船。
-
在玩家飞船下方(3)有一个小矩形,将显示游戏中的信用余额。
-
在右上角(4),一个更大的矩形将包含关于我们选定的升级的信息。它还将包含一个按钮,如果玩家有足够的信用分且/或尚未购买该物品,将给他们提供购买升级的选项。
我们可以从选择网格(商店的按钮行)开始。为了节省时间,我已经为这个场景提供了一些模板艺术,因为我们将在下一章创建自己的 UI 时替换它。
要开始我们的商店选择网格中的第一个按钮,我们需要进入 Unity 编辑器的层次结构窗口并执行以下操作:
- 在以下截图所示的
upgrade的右上角:

图 5.4 – 在层次结构窗口中寻找包含单词"upgrade"的游戏对象
-
现在,选择标题为
UPGRADE_00的顶级游戏对象。信息
注意,除了 Unity 编辑器中层次结构窗口中选定的游戏对象外,场景窗口的内容都是灰色的。这是为了帮助我们定位正在寻找的游戏对象。
-
点击搜索栏右侧的圆形x符号。这将使我们的层次结构内容返回并展开父游戏对象,如下截图所示:

图 5.5 – UPGRADE_00 的子对象
-
按住键盘上的Ctrl(或在 macOS 上按command)并选择三个游戏对象:
-
sprite -
itemText -
SelectionQuad
-
-
在选择了这三个对象后,选择检查器窗口中的左上角的复选框以使这些对象激活。复选框的位置在以下截图中有显示:
![图 5.6 – 使用复选框显示和隐藏游戏对象]()
图 5.6 – 使用复选框显示和隐藏游戏对象
我们的网络应该现在显示其第一个选择,如下面的截图所示:

图 5.7 – 我们的第一个商店按钮
我们的商店已经开始成形。在设置第一个选择后,我们可以在下一节通过代码自定义这些选择。
导入和校准我们的精灵游戏对象
我标记为sprite的游戏对象将接收并显示一个将在选择网格中显示的船升级图像。为了了解这个精灵如何正确显示,我们可以查看当其游戏对象在层次结构窗口中被选中时的属性。
sprite游戏对象附有一个精灵渲染器组件:

图 5.8 – 精灵渲染器组件
我将sprite游戏对象属性灰显,这是我们为powerup属性提供哪种对象类型,这将在场景窗口中给我们一个类似火焰的图标。
让我们检查powerup属性,以确保我们对其数据类型和它在 Unity 编辑器中的识别有把握。
要查看精灵的数据类型,请按照以下步骤操作:
-
在精灵渲染器部分的检查器窗口中,当
sprite游戏对象仍然被选中时。 -
当从精灵渲染器组件中选择时,
powerup精灵的位置将在powerup精灵位置 ping 中出现:

图 5.9 – 我们'powerup'精灵在项目窗口中的位置
- 接下来,点击
powerup精灵导入设置中powerup属性的父级。
大多数信息不需要更改,但主要关注点是确保纹理类型设置为精灵,以便文件与精灵渲染器组件兼容。
以下截图显示了我们的powerup文件被识别为精灵,窗口底部有图像预览:

图 5.10 – 我们的'powerup'文件将在纹理类型字段中设置为精灵
有可能当精灵,如前一个截图中的powerup纹理,被导入 Unity 时,它可能不会被识别为精灵,并将被赋予Default类型。这是因为Default是纹理最常见的选项,尤其是在三维模型中。Default还提供了更多关于纹理属性的选项。
更多信息
如果你想了解更多关于纹理类型的信息,请查看docs.unity3d.com/Manual/TextureTypes.html。
关于我们的powerup纹理,我们不需要将其更改为默认。当我们添加另一个选择时,应该执行相同的检查图像类型的原理。现在让我们继续到UPGRADE_00游戏对象的第二个游戏对象itemText。
在我们的itemText游戏对象上显示信用信息
UPGRADE_00的第二个子游戏对象是itemText。这个游戏对象中的文本包含一个SOLD。
以下截图显示了Text Mesh组件与场景窗口中文本之间的连接:
![Figure 5.11 – Text Mesh text set to '0000']
![img/Figure_5.11_B18381.jpg]
图 5.11 – 将 Text Mesh 文本设置为'0000'
现在让我们继续到最后一个UPGRADE_00层次结构的子游戏对象,即SelectionQuad。
在创建 SelectionQuad 时进行项目文件诊断
在本节中,我将简要解释商店选择网格是如何准备的。
SelectionQuad是UPGRADE_00游戏对象的第三个子游戏对象,如下截图所示:
![Figure 5.12 – The SelectionQuad game object in the Hierarchy window]
![img/Figure_5.12_B18381.jpg]
![Figure 5.12 – The SelectionQuad game object in the Hierarchy window]
这个游戏对象仅仅用来向玩家显示他们已经做出了选择。它由一个四边形网格组成,这是一个在 Unity 中可以创建的标准原语(通过在层次结构窗口中右键单击并选择3D 对象 | 四边形)。我们现在将改变其外观,通过操作其材质值使其看起来更有趣;请按照以下步骤操作:
-
一旦将
Quad对象移动到位置,将其材质属性从不透明渲染模式更改为透明(1)。 -
然后,点击
64、152、255和140。以下截图显示了SelectionQuad材质所做的颜色属性更改:
![Figure 5.13 – Rendering Mode set to Transparent and the RGBA values modified to a light blue color]
![img/Figure_5.13_B18381.jpg]
图 5.13 – 渲染模式设置为透明,并将 RGBA 值修改为浅蓝色
- 那就是我们的
UPGRADE_00选择的全貌。然后,将每个游戏对象复制并粘贴到两个更多的黑色矩形上,并将它们重命名为UPGRADE_01和UPGRADE_02。
以下截图显示了三个游戏对象:
![Figure 5.14 – Duplicating the game objects]
![img/Figure_5.14_B18381.jpg]
图 5.14 – 复制游戏对象
为了本章的目的,我们将使用这三个游戏对象来操作并将信息从一个场景传递到另一个场景。在我们开始为这些选择编写脚本之前,我想向您展示一些将被添加到网格最右侧两个略大的按钮上的文本:
-
在
WATCH AD和START中向下滚动。这两个游戏对象将承担以下责任:-
当玩家选择此按钮时使用
WATCH AD;将播放广告。广告结束后,玩家将获得积分奖励。这些积分可以用来购买更多升级。 -
当玩家完成商店操作时使用
START。他们可以通过按下START按钮继续前进。
-
-
通过点击每个对象左侧的箭头来展开
WATCH AD和START。 -
点击每个游戏对象,并在检查器窗口中使它们处于活动状态,就像我们在介绍我们的商店脚本部分中做的那样。
在每个展开的游戏对象中,我们都有一个标签游戏对象;这个对象包含一个文本网格组件,我们已经在本节中介绍过,用于显示按钮文本。
以下截图显示了层次结构窗口中展开的WATCH AD和START对象:

图 5.15 – WATCH AD和START按钮的位置
到目前为止,我们了解到我们将有一个包含我们飞船升级的可脚本化对象的商店场景;我们也知道观看广告以获得积分是免费游戏中的一个流行机制。
这就是我们选择网格所需的所有内容。现在我们可以开始考虑如何在下一节中开启和关闭按钮,更改每个升级的图标,以及更多操作。
自定义我们的商店选择
在本节中,我们将使用可脚本化对象来定制每个选择。我们已经在第二章,添加和管理对象中使用了可脚本化对象。这次,我们将使用类似的方法,但针对我们的选择网格;希望这能让你欣赏到可脚本化对象如何在游戏中扩展和使用。
如第二章中所述,添加和管理对象,我习惯于使用SO前缀初始化可脚本化对象,以便于识别。让我们创建一个SOShopSelection脚本:
-
在 Unity 编辑器中,转到
Assets/Script。 -
创建一个脚本(使用与第二章**, 添加和管理对象)相同的方法)并命名为
SOShopSelection。
这个SOShopSelection脚本将为我们的资产文件(与我们的玩家和敌舰相同)创建数据类型模板。这些资产文件将附加到每个玩家飞船的升级上。
网格中的单个选择将包含四种属性类型,如下所示:
-
icon:选择图片。 -
iconName:标识选择的内容。 -
description:用于描述在场景右上角的大选择框中升级的内容。 -
cost:计算升级值,以便在选择的积分值中显示。
让我们打开SOShopSelection脚本并开始编码:
-
在脚本顶部,确保我们已导入以下库:
using UnityEngine;
与 Unity 中大多数脚本一样,我们需要 UnityEngine 库,以便我们可以使用 ScriptableObject 功能。
-
为了能够从可脚本对象中创建资产,我们在类名上方输入以下属性:
[CreateAssetMenu(fileName = "Create Shop Piece", menuName = "Create Shop Piece")] -
输入以下代码以将
ScriptableObject继承到SOShopSelection脚本中。这将为我们提供创建模板资产文件的功能:public class SOShopSelection : ScriptableObject { -
输入以下代码以保存特定的变量:
public Sprite icon; public string iconName; public string description; public string cost; } -
保存脚本。
我们已经编写了脚本以保存网格中每个潜在选择的资料。如前所述,我们之前已经创建了这些类型的脚本 – 我们只是在这里使用它们来定制按钮,而不是宇宙飞船。
现在,我们可以在 Unity 编辑器中自定义我们的三个 UPGRADE 游戏对象 – 让我们接下来这么做。
创建选择模板
在最后一节中,我们创建了一个可脚本对象,允许我们创建一个包含自定义参数和值的资产文件。这些资产及其属性可以由不具备编程知识的用户创建,这对于设计师和程序员来说是非常理想的。
我们有三个选择要添加到我们的选择网格中:
-
武器升级:赋予玩家飞船更强的武器
-
健康升级:允许玩家的飞船被敌人对象击中两次
-
原子弹:清除所有可见的敌人
因此,让我们回到 Unity 编辑器,并为我们的选择网格创建一些资产模板:
-
从
Assets/ScriptableObject。 -
在 项目 窗口的空白区域右键单击,然后从出现的下拉菜单中选择 创建,然后选择 创建 Shop Piece,如图下所示截图:

图 5.16 – 创建 'Shop Piece'
-
将新的
Create Shop Piece文件重命名为Shot_PowerUp。 -
当
Shot_PowerUp仍然被选中时,导航到 检查器 窗口,在那里我们有可以输入的数据类型。
以下截图显示了我们将要更改的 Shot_PowerUp 属性:

图 5.17 – 射击 _ 增强属性
-
我们将通过点击其字段右侧的小圆圈,将我们的
powerup精灵图标应用到 图标 数据类型。 -
在
powerup精灵中向下滚动,然后双击,如图下所示截图:

图 5.18 – 更新商店按钮图标
-
现在,输入以下属性名称以及我们将赋予它们的值:
-
b. 射击 -
Blast Shot -
400
-
-
如同之前所做的那样,创建另一个
ShopPiece资产。 -
这次,将资产名称从
Create Shop Piece更改为Health_Level1,并给出以下截图中的详细信息:

图 5.19 – 健康按钮属性
- 现在,让我们制作第三个资产文件,使用与最后两个资产完全相同的流程,但这次将其命名为
Bomb_Cluster并给出以下详细信息:

图 5.20 – 炸弹集群按钮属性
我们已经制作了可脚本对象并为其飞船的升级进行了配置。现在让我们制作第二个主要脚本,即ShopPiece。此脚本将包含我们刚刚制作的每个资产文件,并在商店的网格场景周围显示其内容。
定制我们的玩家飞船升级选择
此脚本的目的是将信息发送到我们商店场景中的每个选择按钮。对于三个UPGRADE游戏对象中的每一个,我们将创建并附加一个脚本,该脚本引用SOShopSelection可脚本对象(我们在上一节中制作的三个资产文件)并将它们分配给每个玩家飞船的升级按钮。
要创建ShopPiece脚本,请执行以下操作:
-
让我们从项目窗口导航到
Assets/Script开始。 -
以与在定制我们的商店选择部分开始时相同的方式创建脚本,并将脚本命名为
ShopPiece。 -
打开脚本并开始编写代码,从脚本顶部的
UnityEngine库开始:using UnityEngine;
由于我们正在使用 Unity 的元素并将ShopPiece脚本附加到 Unity 编辑器中,因此此脚本将需要UnityEngine库。
-
检查并输入以下代码以声明类名和继承:
public class ShopPiece : MonoBehaviour {
默认情况下,我们的脚本应该自动命名,并继承Monobehaviour。
-
输入以下代码以允许更新
shopSelection实例:[SerializeField] SOShopSelection shopSelection; public SOShopSelection ShopSelection { get {return shopSelection; } set {shopSelection = value; } }
我们添加了对上一节中制作的SOShopSelection脚本的引用。此引用是私有的(因为它没有标记为public),但我们通过其上方的[SerializeField]函数将其暴露给 Unity 编辑器。这意味着我们可以将每个可脚本资产文件拖放到 Unity 编辑器中的字段。如果另一个脚本需要访问私有的shopSelection变量,我们可以引用将接收和发送其数据的ShopSelection属性(get和set)。
-
输入以下代码以更新
shopSelection精灵:void Awake() { //icon slot if (GetComponentInChildren<SpriteRenderer>() != null) { GetComponentInChildren<SpriteRenderer>().sprite = shopSelection.icon; }
当此脚本首次激活并运行时,它将运行其Awake函数。在函数内部有两个if语句;第一个检查其任何子游戏对象中是否有SpriteRenderer组件。如果有,则从其shopSelection资产文件中获取引用并将其图标精灵应用到按钮上显示。
-
输入以下代码以更新成本值:
//selection value if(transform.Find("itemText")) { GetComponentInChildren<TextMesh>().text = shopSelection.cost; } }
替代if语句检查此类的任何子游戏对象是否有一个名为itemText的游戏对象。
如果有一个名为itemText的游戏对象,我们将其TextMesh组件的text值更新为shopSelection成本值。
- 保存脚本。
回到 Unity 编辑器,我们可以附加 ShopPiece 脚本,以及我们在上一节中制作的每个资产脚本。
要将每个 ShopPiece 脚本附加到每个 UPGRADE 游戏对象文件,请按照以下步骤操作:
-
从
UPGRADE_00游戏对象。 -
您可以从
Assets /Script中拖放ShopPiece脚本,或者在 检查器 窗口中点击 添加组件 并在下拉搜索中输入脚本的名称。 -
UPGRADE_00将是我们的武器升级按钮,因此对于Assets /ScriptableObject或点击Shot_PowerUp右侧的小圆圈,使用以下截图作为参考,以查看您的 检查器 窗口应如何显示:

图 5.21 – 包含 'Shop_PowerUp' 可脚本对象的 'Shop Piece' 脚本
- 现在,重复此过程为
UPGRADE_01,给游戏对象添加ShopPiece脚本,并在其ShopPiece组件中使用Health_Level1资产为UPGRADE_01:

图 5.22 – 包含 'Health_Level1' 可脚本对象的 'Shop Piece' 脚本
- 最后,为
UPGRADE_02完成相同的程序,添加ShopPiece脚本,并将Bomb_Cluster应用到ShopPiece脚本中:

图 5.23 – 包含 'Bomb_Cluster' 可脚本对象的 'Shop Piece' 脚本
- 要测试三个选择组件是否在选择网格中正常工作,请保存当前场景,并在 Unity 编辑器中点击 播放 按钮。
我们的选择网格应从顶部三个具有相同图像和相同值(不在播放模式中)的按钮开始,到每个图像和值都不同(在播放模式中):

图 5.24 – 在播放模式中更新价格的商店按钮
如果您还记得,我们的资产文件有名称和描述数据;当在商店场景中提供有关物品的信息时,我们可以使用这些数据。此外,我们还需要更新玩家的飞船视觉效果,以显示在飞船上购买的外观,以及一些其他事情。在下一节中,我们将创建一个脚本,允许玩家从选择网格中按按钮。
使用射线投射选择游戏对象
在本节中,我们将创建最终的商店脚本 PlayerShipBuild。此脚本包含选择选择网格中的任何按钮、运行广告、与现有的游戏框架脚本通信、启动游戏进行播放以及其他我们将要介绍的一些功能。
你在 Unity 程序员考试中可能会遇到的一个主题,以及在 Unity 中开发游戏/应用程序时,是发射不可见的激光,这些激光用于射击枪、在三维空间中进行选择等。在本节中,我们将创建一个按钮,当玩家按下它时,选择网格上的按钮会亮起蓝色,以便让他们知道它已被选中。我们已经在每个按钮上设置了永久开启的蓝色矩形。因此,我们现在需要做的只是当场景变得活跃时关闭它们,并在射线(不可见激光)与它们接触时打开任何按钮。
以下截图显示了玩家看到的 2D 选择屏幕(选择 2D)以及从角度看到的相同场景,以便我们可以看到主摄像机的裁剪平面(选择 3D)。当玩家按下选择网格上的按钮时,一条不可见的线(射线)将穿过它。如果这条线接触到附加了碰撞器的游戏对象,我们可以从该游戏对象获取信息:

图 5.25 – 从我们的摄像机位置发射射线以选择商店中的按钮
因此,对于这个选择,我们将开始创建一个PlayerShipBuild脚本,并赋予它发射射线的能力,这将改变按钮的颜色。
让我们从在常规Assets/Script目录中创建一个名为PlayerShipBuild的脚本开始。你现在应该知道如何创建脚本,因为我们已经在之前的自定义我们的玩家飞船升级选择部分中这样做过了。
要创建射线选择,打开PlayerShipBuild脚本并按照以下步骤操作:
-
默认情况下,我们要求使用 Unity 引擎库,如前所述:
using UnityEngine; -
输入以下代码以声明我们的类:
public class PlayerShipBuild : MonoBehaviour {
我们的脚本有一个public访问修饰符,并且与PlayerShipBuild文件同名。
此脚本继承自MonoBehaviour,因此在编辑器中附加到游戏对象时会被识别。
-
输入以下代码以保持每个
shopButtons:[SerializeField] GameObject[] shopButtons;
我们有一个private变量,在编辑器中通过[SerializeField]暴露(因此我们可以查看和编辑它),它将保存选择网格上所有 10 个游戏对象按钮的数组。
-
输入以下代码以保持两个用于射线的游戏对象:
GameObject target; GameObject tmpSelection;
tmpSelection变量用于存储射线选择,以便我们可以检查我们接触到了什么。目标变量将在脚本中稍后使用。
tmpSelection将在选择过程结束时使用,用于打开游戏对象。
-
在
Start函数中输入以下代码以运行我们的方法:void Start() { TurnOffSelectionHighlights(); }
当此脚本变得活跃时,Unity 的Start函数将是首先被调用的。
-
接下来,我们将输入以下代码以创建
TurnOffSelectionHighlights方法:void TurnOffSelectionHighlights() { for (int i = 0; i < shopButtons.Length; i++) { shopButtons[i].SetActive(false); } }
在 TurnOffSelectionHighlights 方法中,我们运行一个 for 循环以确保所有按钮的蓝色矩形都被关闭。
-
将以下代码输入到每帧调用的
Update函数中:void Update() { AttemptSelection(); }
AttemptSelection 方法负责接收玩家对按钮选择的输入。当涉及到这一点时,我们将详细讨论这个方法的内容。
-
将以下代码输入以创建我们的
ReturnClickedObject方法:GameObject ReturnClickedObject (out RaycastHit hit) { GameObject target = null; Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition); if (Physics.Raycast (ray.origin, ray.direction * 100, out hit)) { target = hit.collider.gameObject; } return target; }
ReturnClickedObject 方法也接受一个 out 射线投射命中的参数,该参数将包含射线接触到的碰撞器的信息。
在此方法中,我们将 target 游戏对象重置以删除任何以前的数据。然后,我们从相机获取玩家在屏幕上触摸或点击鼠标的位置,并将结果以射线的形式存储。
更多信息
关于 ScreenPointToRay 的更多信息可以在 Unity 的脚本参考中找到,链接为 docs.unity3d.com/ScriptReference/Camera.ScreenPointToRay.html。
然后,我们检查从我们发射射线的地方,相机的原点和方向是否与碰撞器(在 100 个世界空间米内)接触。
如果我们与碰撞器接触,if 语句被认可为 true;然后我们获取它所击中的游戏对象的引用并将其存储在 target 游戏对象中。
最后,我们发送出(return)射线接触到的 target 游戏对象。
如果你还记得,我们在 Update 函数中之前提到了一个 AttemptSelection 方法。AttemptSelection 方法将检查当玩家通过触摸屏幕或点击鼠标按钮在我们的商店场景中接触时是否满足条件。
-
将以下代码输入以编写
AttemptSelection方法:void AttemptSelection() { if (Input.GetMouseButtonDown (0)) { RaycastHit hitInfo; target = ReturnClickedObject (out hitInfo); if (target != null) { if (target.transform.Find("itemText")) { TurnOffSelectionHighlights(); Select(); } } } }
如果玩家按下了鼠标按钮或触摸了触摸屏,我们将发射射线并将所有 RaycastHit 对象发送到我们在上一节代码中提到的 ReturnClickedObject 方法。ReturnClickedObject 方法的返回结果将返回到我们在脚本开头创建的 target 游戏对象。
然后,我们检查这个 target 游戏对象是否存在。如果它确实存在,我们接着检查这个 target 游戏对象是否持有名为 itemText 的游戏对象。如果它确实有一个名为此的游戏对象,我们将通过关闭所有蓝色四边形并调用名为 Select 的方法来刷新选择网格,这是我们接下来要讨论的内容。
我们最终深入到脚本中的最后一段,在那里我们找到了 SelectionQuad 游戏对象的名称,并将其打开以给玩家提供他们所做选择的视觉反馈。
-
在我们的代码中输入以下方法;这将使玩家的按钮选择变为活动状态:
void Select() { tmpSelection = target.transform.Find ("SelectionQuad").gameObject; tmpSelection.SetActive(true); }
Select 方法不需要使用 if 语句检查任何条件,因为大部分工作已经由之前的代码为我们完成。我们将执行对 SelectionQuad 的搜索,并将其引用存储为 tmpSelection。
最后,我们将 tmpSelection 游戏对象的活动设置为 true,以便在 shop 场景窗口中可见。
- 保存脚本并返回到 Unity 编辑器。
我们现在可以将 PlayerShipBuild 脚本附加到我们的商店游戏对象上(使用本章前面为 ShopPiece 使用的相同附加方法),这是选择网格中所有按钮的父对象,如下面的截图所示:
![图 5.26 – 包含 'Player Ship Build' 脚本和突出显示的 'Shop Buttons' 数量的 'shop'(0)
![img/Figure_5.26_B18381.jpg]
图 5.26 – 包含 'Player Ship Build' 脚本和突出显示的 'Shop Buttons' 数量的 'shop'(0)
此外,如果您记得在 PlayerShipBuild 脚本的开始部分,我们添加了一个游戏对象变量,该变量将接受一个 shopButtons 数组。我们可以在脚本开始时使用 for 循环自动添加每个 UPGRADE 游戏对象,但如果将来我们想要考虑使用游戏手柄或键盘来引导我们通过选择网格,我们将对知道哪个按钮分配给每个数组编号有更多的控制。这也是一种不依赖代码的编程方式,因为 Unity 是一个基于组件的引擎。其他控制器输入和交互是我们将在 第十三章,效果、测试、性能和替代控制)中讨论的内容,我们将开始考虑将游戏移植到其他平台。
在后续课程中更新我们的控制功能,以下是您应该在 Unity 编辑器中将游戏对象附加到 shopButtons 数组的方法:
-
在 Hierarchy 窗口中仍然选择商店游戏对象时,此时锁定 Inspector 窗口可能是最好的选择(正如我们在 第四章,应用艺术、动画和粒子)中做的),因为我们将要选择和拖动不同的游戏对象。
-
在 Inspector 窗口中将商店按钮的大小从
0更改为10。
现在,我们将为每个 SelectionQuad 游戏对象获得一系列空的游戏对象字段。
- 接下来,为了使事情更加简单,点击商店游戏对象下每个游戏对象左侧的箭头以展开每个子游戏对象。这将揭示我们需要拖动的
SelectionQuad游戏对象。
以下截图显示了包含空游戏对象列表的 Inspector 窗口和展开的 Hierarchy 窗口游戏对象:
![图 5.27 – 将每个 SelectionQuad 游戏对象从 Hierarchy 窗口拖放到 Inspector 窗口中的正确字段]
![图 5.27 – Figure_5.27_B18381.jpg]
图 5.27 – 将 Hierarchy 窗口中的每个 SelectionQuad 游戏对象拖放到 Inspector 窗口的正确字段中
我们还在之前的屏幕截图上添加了箭头条纹,以显示哪些SelectionQuad对象需要放入Inspector窗口的哪个游戏对象字段。
-
保存场景。如果你锁定了Inspector窗口,别忘了解锁它。
-
在编辑器中按下播放按钮。
现在,当场景开始时,所有的蓝色选择四边形都会消失,但如果你点击任何一个,它将根据按下的哪个按钮而亮起。
以下截图显示了当鼠标在游戏窗口中点击它时选择的原子弹按钮。这也适用于触摸屏设备:
![图 5.28 – 选择的原子弹按钮
![图 5.28 – Figure_5.28_B18381.jpg]
图 5.28 – 选择的原子弹按钮
这就涵盖了使用射线投射,这是一种可转移的技能,可以用于任何涉及射击以从另一个游戏对象中获取信息的情况,前提是它附有碰撞器。
让我们现在继续更新描述面板,以便当从网格中选择一个选项时,我们能够从存储在相同可脚本对象中的信息获取大暗矩形上的文本,这些可脚本对象为每个玩家升级按钮提供信息。
向我们的描述面板添加信息
当在商店场景中选择一个选项时,我们可以从选择的脚本对象资产文件中获取信息,并在其textBoxPanel游戏对象内显示其详细信息。
让我们看看Hierarchy窗口中的textBoxPanel对象:
![图 5.29 – Hierarchy 窗口中的我们的'textBoxPanel'内容
![图 5.29 – Figure_5.29_B18381.jpg]
图 5.29 – Hierarchy 窗口中的我们的'textBoxPanel'内容
textBoxPanel游戏对象包含一个用于其背景的黑色四边形。它还包含其他四个游戏对象,如下所示:
-
name:此游戏对象包含一个iconName可脚本对象变量。 -
desc:此游戏对象还包含一个description可脚本对象变量。 -
backPanel:此游戏对象作为选择网格的背景。 -
BUY?:当我们要确认购买所选项目时,将在稍后讨论此游戏对象。
以下截图标识了从我们本章早期制作的Health_Level1资产文件中获取的两个脚本对象数据类型。这个大矩形上的信息将根据所选按钮而改变:
![图 5.30 – 在矩形框中选择的按钮的描述
![图 5.30 – Figure_5.30_B18381.jpg]
图 5.30 – 在矩形框中选择的按钮的描述
让我们现在回到PlayerShipBuild脚本,并添加一些代码来更新描述面板(textBoxPanel游戏对象):
-
重新打开
PlayerShipBuild脚本,并将以下变量添加到脚本顶部,与其他变量一起:GameObject textBoxPanel;
这个游戏对象变量将保存对场景中 textBoxPanel 游戏对象的引用。接下来,我们需要从 层次结构 窗口中获取并引用这个游戏对象。
-
滚动到
Start函数,并输入以下代码行以将textBoxPanel游戏对象作为引用存储:textBoxPanel = GameObject.Find("textBoxPanel");
现在,滚动到 AttemptSelection 的内容。
-
滚动到以下
if语句,并将UpdateDescriptionBox();添加到该语句的内容中:if (target.transform.Find("itemText")) { TurnOffSelectionHighlights(); Select(); UpdateDescriptionBox(); }
UpdateDescriptionBox 方法将获取所选按钮的两个资产。这些资产(iconName 和 description)随后被应用到 textboxPanel 的每个 TextMesh text 组件上。
-
现在我们用以下代码进入这个方法的正文:
void UpdateDescriptionBox() { textBoxPanel.transform.Find("name").gameObject.GetComponent <TextMesh>().text = tmpSelection.GetComponentInParent <ShopPiece>().ShopSelection.iconName; textBoxPanel.transform.Find("desc").gameObject.GetComponent <TextMesh>().text = tmpSelection.GetComponentInParent <ShopPiece>().ShopSelection.description; }
UpdateDescriptionBox 方法将获取所选商店按钮的引用名称和描述,并将字符串值应用到商店的黑色公告板上(textBoxPanel)。
-
保存脚本。
-
通过在 Unity 编辑器中按 播放 测试结果。
以下截图显示了第一个选择网格被选中,并显示了描述面板的详细信息:


图 5.31 – 选择‘爆破射击’武器时显示的描述
用少量的代码,描述面板就会亮起并显示所选任何物品的信息。这很有用,因为我们如果想要通过添加更多物品来扩展选择网格,我们就不需要膨胀我们的代码来补偿每个选择。
现在我们有一个显示可购买内容和每个物品描述的商店场景。让我们总结一下本章我们学到了什么。
摘要
在本章中,我们开始创建一个包含由三维多边形创建的各种按钮和面板的商店场景。然后我们创建了自己的脚本,用图像、值、名称和描述填充场景,这些资产可以用虚拟信用购买。
我们还利用可脚本化的对象创建了一个代码模板,这样它就可以通过添加各种游戏内增强功能来补充,而不会膨胀我们的游戏框架。我们还使其可互换,如果需要更换、替换或删除武器,我们只需简单地删除模板,而不会影响游戏框架中的其他代码。
本章我们学到的另一课是意识到我们可以创建免费下载的游戏,同时也要知道如何通过货币化游戏设计创建收入形式。
在下一章中,我们将继续我们的商店场景,通过向玩家的飞船添加内容,更多地关注每个按钮的内容和商店的整体功能。我们还将探讨游戏广告作为玩家升级飞船的一种货币形式。
第六章:第六章:购买游戏内物品和广告
在本章中,我们将继续构建我们的商店场景,通过添加功能,例如引入玩家的游戏内货币,并查看如何扣除和增加它。
赚钱是指游戏可以免费下载(通常称为免费游玩),开发者鼓励或提供玩家用真实货币(使用银行卡/借记卡)购买物品,例如最新的武器、额外的艺术修改视觉效果等。从免费游玩游戏中创造利润的另一种方式是提供融入游戏中的广告。例如,如果玩家想要一艘新船或额外的生命,他们可以观看 30 秒的广告,而无需支付任何真实货币,但作为开发者,我们可以在广告被观看时获得收入。当然,在制作游戏时必须考虑平衡,一些公司会使用各种上瘾心理学来鼓励玩家购买升级或观看尽可能多的广告。这可能导致单个玩家有时支付数千真实货币。如何规划并制作自己的游戏取决于你,但就本章而言,我们将创建自己的商店,为玩家提供观看广告以获得额外游戏币的机会。
在本章中,我们将涵盖以下主题:
-
为我们玩家的飞船购买升级
-
购买物品、观看广告以及准备开始游戏
-
扩展
PlayerSpawner脚本
到本章结束时,我们将了解何时触发广告以及如何奖励玩家,同时提供持续的反馈。我们将利用资产商店和 Unity 的在线仪表板系统。
让我们开始吧!
本章涵盖的核心考试技能
在本章中,我们将涵盖编程核心交互:
-
实现和配置游戏对象行为和物理
-
实现和配置输入和控制
我们还将涵盖应用程序系统的开发:
-
解释应用程序界面流程的脚本,例如菜单系统、UI 导航和应用程序设置
-
解释用户控制的定制的脚本,例如角色创建器、库存、店面和在应用内购买
-
分析用于用户进度功能的脚本,例如得分、等级和游戏内经济,利用 Unity Analytics 和 PlayerPrefs 等技术
-
分析二维叠加的脚本,例如抬头显示(HUDs)、小地图和广告
-
识别用于保存和检索应用程序和用户数据的脚本
我们还将涵盖场景和环境设计的编程:
- 识别实现游戏对象实例化、销毁和管理的方法
最后,我们将涵盖在专业软件开发团队中的工作:
- 识别用于构建模块化、可读性和可重用性的脚本结构技术
技术要求
本章的项目内容可以在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_06找到。
您可以下载每个章节的项目文件的全部内容,地址为github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition。
本章的所有内容都存储在该章节的unitypackage文件中。本章节没有Complete文件夹。
观看以下视频,查看代码执行情况:bit.ly/3MASWFJ。
为我们的玩家船只购买升级
在本节中,我们将介绍为我们的玩家船只购买升级的过程。这包括以下内容:
-
信用余额
-
购买选项
-
让玩家知道物品已被售出
以下截图显示了我们的商店场景及其选择网格和两个标记为已售的已购买物品。在选择网格上方左侧是用户的当前游戏内银行余额,以及显示玩家船只应用两个升级后的外观的图片。最后,在右侧是购买当前选中物品的选项,该物品为C.炸弹:

图 6.1 – 我们的商店场景,包含货币、售罄标志和购买选项
在本节中,我们将回到负责购买升级并将其应用于玩家船只的脚本。在PlayerShipBuild脚本中,我们将添加全局变量,这些变量将持有玩家的武器升级数组、游戏对象按钮以及玩家的银行余额。
然后,我们将将这些新变量连接到场景中的文本和游戏对象按钮,并从那里添加我们自己的方法来开启或关闭按钮,并确定玩家是否有足够的游戏内信用额进行购买。
让我们从将这些新变量输入我们的商店场景开始,方法是进入Assets/Script文件夹中的PlayerShipBuild脚本:
-
将以下变量输入到
PlayerShipBuild脚本中:[SerializeField] GameObject[] visualWeapons; [SerializeField] SOActorModel defaultPlayerShip; GameObject playerShip; GameObject buyButton; GameObject bankObj; int bank = 600; bool purchaseMade = false;
我们主要添加了包含商店场景可视化表示的游戏对象,但我们还添加了一个可脚本化的对象,用于为玩家的船只提供其自己的属性值,例如速度、健康、使用的子弹类型等。我们将在下一个代码块中使用一些这些变量。
-
接下来,我们将通过在
Start函数中获取银行游戏对象和商店场景中的购买?按钮来更新PlayerShipBuild脚本:purchaseMade = false; bankObj = GameObject.Find("bank"); bankObj.GetComponentInChildren<TextMesh>().text = bank.ToString(); buyButton = textBoxPanel.transform.Find("BUY ?").gameObject; TurnOffPlayerShipVisuals(); PreparePlayerShipForUpgrade();
此代码在 Unity 编辑器的层次结构窗口中重置或分配变量到游戏对象。我将在这里简要解释这些变量,并在使用它们时进行更详细的说明:
-
purchaseMade是一个布尔变量,它只接受true或false值。我们在这里将其设置为false作为重置的一种形式。 -
bankObj:在bank中。我们将为此变量分配该游戏对象,以供以后使用。 -
然后,我们取
bank整数,当前包含的值为600,并将其转换为string值,以便在商店场景中显示在玩家飞船的三维模型下方。 -
最后一个变量被分配给
BUY ?游戏对象,这样我们就可以在场景或游戏窗口中随时激活和停用购买功能。 -
TurnOffPlayerShipVisuals:此方法将重置玩家的飞船的视觉效果。 -
PreparePlayerShipForUpgrade:此方法创建玩家的飞船,以便当它应用了所有升级后,可以发送到游戏中进行游玩。
-
现在我们已经创建了变量并分配了它们,我们可以继续到代码的条件部分。在脚本中向下滚动,直到你到达以下行:
if (target.transform.Find("itemText")) -
在
if语句中,我们将检查我们试图购买的商品是否在商店中未售罄(商品售罄的唯一原因是我们已经购买了它),并且我们是否有能力购买:if (target.transform.Find("itemText")) { TurnOffSelectionHighlights(); Select(); UpdateDescriptionBox(); //NOT ALREADY SOLD if (target.transform.Find("itemText").GetComponent<TextMesh>(). text != "SOLD") { //can afford Affordable(); //can not afford LackOfCredits(); } else if (target.transform.Find("itemText").GetComponent <TextMesh>().text == "SOLD") { SoldOut(); } }
我们首先输入一条注释来通知自己或任何其他程序员,在代码的这个位置,我们将检查我们试图购买的商品是否已经售罄。从这里,我们添加一个if语句条件,检查target变量(如前一章中使用射线选择游戏对象部分所述的射线投射的商品)是否包含一个持有string(文本)值的TextMesh组件,且该值不是"SOLD"。如果是,那么我们运行SoldOut方法。
如果商品尚未售罄,那么我们将运行两个方法——第一个是Affordable,这意味着我们将检查我们是否可以用当前拥有的信用额度购买该商品。如果我们没有足够的信用额度,将运行LackOfCredits方法。
我们已经创建了三个新方法,分别称为Affordable、LackOfCredits和SoldOut;现在让我们逐一介绍它们,从Affordable开始。
-
在
AttemptSelection方法外部,添加以下代码:void Affordable() { if (bank >= System.Int32.Parse(target.transform. GetComponent<ShopPiece>().ShopSelection.cost)) { Debug.Log("CAN BUY"); buyButton.SetActive(true); } }
Affordable方法检查bank整数(当前包含的值为600)是否等于或大于我们已选中的按钮的值(target)。
接下来是一个if语句,用于检查bank整数值是否大于或等于所选项目的string cost值。因为我们不能将string变量的值与int变量的值进行比较,所以我们需要将string变量转换为int变量。为此,我们使用System.Int32.Parse()并将ShopSelection.cost字符串值输入到解析括号中。
如果我们可以购买该物品,则将buyButton设置为活动状态,这是一个玩家可以按下来购买物品的按钮。在buyButton.SetActive(true)之上是对 Unity 的控制台窗口的记录,以确认正在进行的购买,用于错误检查。
-
我们之前编写的第二个方法是
LackOfCredits方法,它以类似的方式检查,通过将TextMesh组件值强制转换为小于bank整数值。如果是这样,我们向 Unity 的.Find发送"CAN'T BUY"消息。与单独使用.GetComponent相比,.Find要慢得多。.Find必须遍历每个游戏对象,直到找到匹配的字符串——如果它甚至存在。我们还可以比较性能与灵活性——例如,
transform.GetChild(docs.unity3d.com/ScriptReference/Transform.GetChild.html)将获取其参数中指定的特定子对象,这比使用.Find更快。然而,如果在开发过程中游戏对象层次结构发生了变化,这会导致错误,因为它将无法找到游戏对象。同样,对于.GetComponent也可以说,如果它在代码中不存在,它会导致错误。 -
第三个是
SoldOut方法,目前设置为将SOLD OUT记录到 Unity 编辑器,但同样,我们可以在稍后添加其他功能,例如应用音效或播放动画:void SoldOut() { Debug.Log("SOLD OUT"); } -
创建两个空方法。我们将在本章稍后填充它们的内容:
void TurnOffPlayerShipVisuals() { } void PreparePlayerShipForUpgrade() { } -
保存脚本并返回到 Unity 编辑器。
反思本节,我们在脚本开始时使用Start函数编码了我们的变量并分配了它们。我们还编写了一些方法,用于检查余额与所选值之间的比较。
现在,我们可以继续更新商店场景中玩家的飞船视觉效果,并且我们还可以看到玩家在游戏中的飞船外观。
更新玩家飞船的视觉表示
在本节中,我们将使用能力进行编码,以便在购买时更新玩家的飞船视觉效果,并在幕后创建和更新另一艘飞船,以便将其发送到下一个场景进行游戏。
在上一章的使用射线选择游戏对象部分,我们将SelectionQuad游戏对象从层次结构窗口拖放到检查器窗口。
以下截图显示检查器窗口上的大多数shop游戏对象被灰色显示,这样我们就可以专注于视觉武器下的新变量条目:
![图 6.2 – 显示将附加到玩家飞船上的视觉武器]

图 6.2 – 显示将附加到玩家飞船上的视觉武器
为了更新潜在玩家飞船升级的住房,我们需要将以下内容应用于Visual Weapons游戏对象数组,并参考前面的截图:
-
改变
3。 -
对于三个空的游戏对象字段,点击字段右侧的圆圈,并在下拉菜单中,开始在搜索栏中输入
energy +1。 -
一旦看到energy + 1,双击它。
-
对于
c. Bomb重复此过程。 -
对于
b. Shot重复此过程。 -
最后,通过它右侧的小远程圆圈更新
Player_Default资产文件。当我们将其应用于代码实践时,我们将对此进行更详细的说明。 -
保存场景并返回到
PlayerShipBuild脚本。 -
我们现在可以输入我们的
TurnOffPlayerShipVisuals方法的内容。此方法在Start函数中实现,以简单地重置场景,使得唯一的视觉表示就是玩家的飞船的三维模型:void TurnOffPlayerShipVisuals() { for (int i = 0; i < visualWeapons.Length; i++) { visualWeapons[i].gameObject.SetActive(false); } }
代码运行一个for循环,遍历我们拖放到检查器窗口的Visual Weapons对象数组中的每个游戏对象。
我们已经更新了玩家的飞船模型,以便在购买物品时,只需通过操纵游戏对象的活动,它就会在场景/游戏窗口中更新。
现在,我们将更多地关注玩家的飞船代码以及选择网格上的另外两个按钮——购买?和观看广告。
准备玩家的飞船以便在游戏中使用
本节旨在准备我们的玩家飞船,以便它可以被发送到下一个场景进行游戏。我们首先创建一个标准的飞船,玩家除了看到它的视觉表示外(场景中有两艘飞船,但玩家只能看到一艘)将看不到它。
因此,如果玩家在我们的商店中进行了某些购买,我们需要创建一艘飞船,并添加其视觉和物理升级,以便我们可以在下一个场景中看到它的实际效果。
我们需要回到PlayerShipBuild脚本,并将内容添加到我们的空PreparePlayerShipForUpgrade方法中,以帮助支持制作带有新升级的玩家飞船:
void PreparePlayerShipForUpgrade()
{
playerShip = GameObject.Instantiate(defaultPlayerShip. actor);
playerShip.GetComponent<Player>().enabled = false;
playerShip.transform.position = new Vector3(0,10000,0);
playerShip.GetComponent<IActorTemplate>
().ActorStats(defaultPlayerShip);
}
此方法从Resources文件夹创建(实例化)一个Player_Ship游戏对象。然后我们关闭(enabled = false)其自身的脚本附加;否则,我们将在商店场景中能够移动和射击它。
然后,我们将Player_Ship对象完全移出场景/游戏窗口视图。
最后,我们将它分配给我们在上一节中拖放到检查器窗口脚本对象字段中的defaultPlayerShip资产文件。
在本节中,我们重新访问了PlayerShipBuild脚本,并添加了更多全局变量和功能来支持商店场景。现在我们的游戏有一个游戏内信用分数,并计算出玩家是否能够负担得起游戏物品;本节中其余的代码是为了隐藏游戏对象并准备玩家的飞船以便将其带入游戏场景。
在下一节中,我们将继续编写PlayerShipBuild脚本,并探讨实际上开始游戏与玩家的飞船一起玩。我们还将查看玩家如何通过使用 Unity 仪表板和 Unity Monetization 从资源商店观看广告来购买游戏内信用。
购买物品、观看广告并准备开始游戏
在本节中,我们将查看向我们的商店场景添加三个更多按钮。第一个是“购买?”当我们想要购买物品时使用。第二个是“观看广告”——一旦玩家按下此按钮,就会加载广告;一旦完成,玩家将获得300信用。最后,我们将添加“开始”按钮,该按钮将玩家带到他们已购买的升级(如果有)的testLevel场景。
我们需要回到PlayerShipBuild脚本,并滚动到AttemptSelection方法,在那里我们将添加三个else if语句以启动三种不同类型的方法。这样做的原因是这三个选择并不跟随可脚本化对象按钮;因此,这些物品永远不会出现已售出或itemText等结果。
以下截图显示了完整的AttemptSelection方法,重点放在其他三个非脚本化对象按钮上:

图 6.3 – 当按下时,我们的商店按钮有三种不同的加载方法
我们首先将查看“购买?”按钮,因为它与我们本节要讨论的内容相关。
设置“购买?”按钮
在本节中,我们将连接购买?按钮,使其在描述面板中正确的时间出现。此按钮仅在玩家尚未购买物品并且有足够的信用时才会显示。
以下截图显示了我们的商店场景,其中购买?按钮被突出显示:

图 6.4 – “购买?”按钮
让我们从编写PlayerShipBuild脚本开始:
-
打开
PlayerShipBuild脚本。 -
在大括号之后滚动到
AttemptSelection方法,如下面的片段所示:if (target.transform.Find("itemText")) { //CODE WE HAVE ENTERED... //CODE WE HAVE ENTERED... //CODE WE HAVE ENTERED... } <---- BEGIN CODING "ELSE IF" HERE -
添加以下
else if代码,如前述代码所示。我们也可以使用前一小节的截图作为参考:else if(target.name == "BUY ?") { BuyItem(); } -
因此,如果玩家点击
target.name等于BuyItem方法,就会调用该方法。在该方法内部,执行以下代码:void BuyItem() { Debug.Log("PURCHASED"); purchaseMade = true; buyButton.SetActive(false); tmpSelection.SetActive(false);
我们将purchaseMade设置为true。这个布尔值在离开商店场景开始游戏时会被使用。如果purchaseMade是true,则会执行一系列程序。下一行代码关闭了buyButton功能,因为我们不再需要显示结果。最后,我们从屏幕底部的网格中移除选择,以刷新页面。
从BuyItem方法继续,我们现在将注意力转向visualWeapons游戏对象,如果您记得本章前面的内容,它涵盖了我们所购买的内容以及玩家飞船在游戏中看起来会是什么样子。
-
在
BuyItem方法中继续操作,添加以下代码以命名并使所有visualWeapons案例生效:for (int i = 0; i < visualWeapons.Length; i++) { if (visualWeapons[i].name == tmpSelection.transform.parent.gameObject. GetComponent<ShopPiece>().ShopSelection.iconName) { visualWeapons[i].SetActive(true); } }
我们运行一个for循环来计算数组中有多少visualWeapons对象。在if语句中,我们检查数组中的每个visualWeapon名字,看它是否与选择网格的名字匹配。如果匹配,则打开特定的visualWeapon对象,以便在商店选择中看到它。
-
在
BuyItem方法中继续操作,我们添加另一个方法来将我们的升级和bank信用额发送到玩家的飞船上,以下代码如下:UpgradeToShip(tmpSelection.transform.parent.gameObject.GetComponent <ShopPiece>().ShopSelection.iconName); bank = bank - System.Int32.Parse(tmpSelection.transform.parent. GetComponent<ShopPiece>().ShopSelection.cost); bankObj.transform.Find("bankText").GetComponent<TextMesh>().text = bank.ToString(); tmpSelection.transform.parent.transform.Find("itemText"). GetComponent<TextMesh>().text = "SOLD"; }
我们运行另一个名为UpgradeToShip的方法。这个方法将加载我们游戏中购买的项目游戏对象到玩家飞船上;我们将在稍后详细介绍这个方法。
接下来,我们从bank值中扣除(使用System.Int32.Parse,因此它将string值读取为int值)选择的项目cost脚本对象。然后,我们通过从银行的游戏对象获取引用,称为bankText,并在TextMesh组件中更新其text值来表示扣除。
最后,我们更新选择网格中的选择,表示该项目已被售出。这更新到按钮的值文本。
这就结束了BuyItem方法。但是,如前所述,我们运行了UpgradeToShip方法,该方法加载特定船部件的游戏对象并将其附加到一个屏幕外的飞船上。
-
仍然在
PlayerShipBuild脚本中,让我们添加UpgradeToShip方法:void UpgradeToShip(string upgrade) { GameObject shipItem = GameObject.Instantiate(Resources.Load (upgrade)) as GameObject; shipItem.transform.SetParent(playerShip.transform); shipItem.transform.localPosition = Vector3.zero; }
UpgradeToShip方法接受一个名为upgrade的string参数。之前,我们发送了以下代码行:
tmpSelection.transform.parent.gameObject.GetComponent
<ShopPiece>().ShopSelection.iconName
这行代码来自选择的脚本对象项目名字。这个项目的名字(ShopSelection.iconName)作为一个string名字(upgrade)发送到UpgradeToShip。
在UpgradeToShip方法内部,我们从资源文件夹中根据商店选择图标的名字创建(实例化)一个游戏对象,并将其存储在一个游戏对象变量shipItem中。
然后,我们将这个shipItem游戏对象附加到我们的playerShip对象上。这是不在Game窗口视图中的playerShip对象,但它将被发送到下一个场景——游戏场景。
shipItem 游戏对象的本地位置(与其父游戏对象 playerShip 相比的位置)被设置为 0(即其 x、y 和 z 位置被设置为 0)。
-
保存脚本并返回到 Unity 编辑器。
-
点击 Play 按钮开始游戏模式,并在 Game 窗口中选择选择网格中的第一个项目。
我们现在应该能够购买此物品。如果我们点击购买,按钮将不再显示值,而是显示 SOLD,并且如果我们再次尝试选择相同的物品,BUY? 按钮将消失。
我们还剩下两个按钮需要连接,然后我们将拥有一个完全运行的商店。让我们继续处理 START 按钮。
设置 START 按钮
START 是玩家按下以离开商店场景并继续玩游戏时按下的按钮。
以下截图显示了商店场景中 START 按钮的位置:


图 6.5 – START 按钮
因此,我们可以回忆起在前一节中,我们正在 AttemptSelection 方法中编码,该方法在玩家按下商店选择网格上的一个按钮时运行。
在此方法的底部有三个 else if 语句。我们已经设置了一个按钮,即 else if 语句,它是 AttemptSelection 方法:


图 6.6 – START 按钮,将加载 StartGame() 方法
接下来,我们将确保我们的玩家从商店(如果有的话)购买了物品或升级,并加载我们的第一个关卡:
-
因此,在
PlayerBuild脚本中AttemptSelection方法的底部,输入以下if语句:else if(target.name == "START") { StartGame(); }
当我们的 target 游戏对象选择带有 START 游戏对象名称时,我们将进入 else if 语句并运行 StartGame 方法。此方法很小,其大部分代码取决于是否进行了购买。
-
在我们的
PlayerShipBuild脚本中继续,添加StartGame方法:void StartGame() { if (purchaseMade) { playerShip.name = "UpgradedShip"; if (playerShip.transform.Find("energy +1(Clone)")) { playerShip.GetComponent<Player>().Health = 2; } DontDestroyOnLoad(playerShip); } UnityEngine.SceneManagement.SceneManager.LoadScene("testLevel"); }
如果 purchaseMade 设置为 true,我们将进入 if 语句,并将我们的 playerShip 游戏对象命名为 "UpgradedShip"。然后我们检查 playerShip 对象是否为更多健康购买了 ("energy +1(Clone)")。如果玩家购买了更多健康,我们将我们的 playerShip 对象的 health 值设置为 2。这意味着我们的玩家可以在死亡前被击中两次。
DontDestroyOnLoad 函数接受 playerShip 参数,这意味着当下一个场景加载时,playerShip 游戏对象将被带到下一个场景。
最后,我们开始我们的 testLevel 场景。
- 保存脚本。
因此,在购买(或未购买)之后,我们的商店场景将关闭,我们的 testLevel 将打开,无论是否进行了购买。然而,我们将在本章稍后更新我们的 PlayerSpawner 脚本后才能看到视觉升级。
返回 Unity 编辑器并运行播放模式以检查玩家飞船升级是否保留。
让我们现在继续到最后一个if else语句——观看广告按钮。
设置观看广告按钮
在本章中我们将要介绍的最后一个按钮是观看广告按钮。在许多移动设备(Android 和 iOS)的免费游戏(游戏免费玩,但通过游戏内购买或广告赚钱)中,玩家可以通过观看 30 秒的广告来获得升级和修改、获得游戏内积分以及更多。观看广告后,玩家将获得积分。在本节中,我们将使用我们的代码和 Unity 的在线仪表板创建这个功能。
下面的截图显示了选择网格中观看广告按钮的位置:

图 6.7 – 观看广告按钮
我们需要在 Unity 编辑器中打开 Unity 的广告服务并遵循以下步骤:
- 如前所述,如果服务选项卡不可用,我们可以在 Unity 编辑器窗口选项卡下的通用,然后是服务,如下面的截图所示:

图 6.8 – 服务位置
- 点击服务选项卡并选择广告,如下面的截图所示:

图 6.9 – 打开广告
在前面的截图中,您可能在服务窗口的顶部看到一个警告通知,说明您没有项目 ID(在截图中标有*****)。如果您没有这个警告,您可以跳过下一组说明并设置广告服务。
要解决未设置项目 ID 的问题,我们需要通过以下方式附加我们的 ID:
- 当我们在服务窗口中点击广告部分时,会出现项目设置窗口。在项目设置窗口中,服务将从列表中展开,并选择广告,如下面的截图所示:

图 6.10 – 无项目 ID 的项目设置
- 在创建 Unity 项目 ID标题下点击选择组织。
您的组织将在列表中。点击它(这是您使用 Unity 创建账户时创建的。我的名字叫 retrophil)。
- 点击创建项目 ID按钮。
很可能控制台窗口会发出一些警告、错误等。现在请不要担心这些,请将注意力集中在项目设置窗口上。
接下来,您将看到以下项目设置窗口的更新:

图 6.11 – 项目成功链接
让我们继续在我们的项目中设置广告:
-
我们会被问及我们的应用是否针对 13 岁以下的儿童。我们的项目不是针对这个年龄段的儿童,所以从下拉列表中选择否。
-
点击保存。
我们的项目现在已准备好开启广告。
- 点击关闭以将我们的广告服务开启,如下截图所示:
![Figure 6.12 – Ads Package
![img/Figure_6.12_B18381.jpg]
图 6.12 – 广告包
- 最后,我们需要通过仪表板激活货币化服务。为此,在广告部分点击仪表板链接。这将在一个浏览器中加载 Unity 仪表板。在货币化部分点击完成激活,然后Unity Ads。您将获得您的游戏 ID。返回到 Unity 编辑器。
当你阅读这本书的时候,很可能会对广告包进行更新;如果可能的话,仍然安装 3.7.5 版本,因为较晚的版本可能会有不同的/未完成的设置,这意味着以下说明可能不再适用或可能存在未完成的方法。但如果您想(或必须)安装最新版本,请确保遵循 Unity 广告文档:docs.unity.com/ads/UnityAdsHome.html。
我们现在已经将 Unity Ads 连接到我们的项目。项目设置窗口还显示了 Android 和 iOS 操作系统的游戏 ID。这些 ID 是 Google Play 和 Apple App Store 在发送广告时用来识别我们游戏的参考。
每个游戏 ID 都是不同的,所以你的游戏 ID 将不同于我的。当涉及到输入你的游戏 ID 代码时,确保你参考的是你的,而不是我的。
我们现在可以回到我们的PlayerShipBuild脚本,开始将我们的观看广告按钮连接到显示广告。
将 Unity 奖励广告附加到我们的脚本
在本节中,我们将从广告库获取放置信息和额外功能来创建我们的奖励广告:
-
在 Unity 编辑器中,从
Assets/Script文件夹开始,然后按照以下步骤操作。 -
打开
PlayerShipBuild脚本,在需要修改的代码顶部,与我们的UnityEngine库一起导入两个额外的库:using UnityEngine.Advertisements;
System.Collections包含额外的工具,例如IEnumerator,它用于StartCoroutine函数,我们将在稍后介绍。
另一行代码导入了 Unity 的广告库,这是我们从广告服务下载的,它也会在包管理器中显示。同时,确保你在构建设置中选择了 Android 作为你的平台。
什么是包管理器?
Unity 在包管理器中保存了大部分的附加组件/额外软件和组件更新。我们也可以检查这些包的更新。如果你想了解更多关于包管理器的信息,请查看以下链接:docs.unity3d.com/Manual/Packages.html。
接下来,我们将向 PlayerShipBuild 脚本添加两个接口,因为它们对于广告服务的工作是必需的,即 IUnityAdsInitializationListener 和 IUnityAdsListener,并在我们的类中实现这些接口。这些接口将给我们回调方法(回调是在执行特定任务后运行的方法),告诉我们广告的状态以及何时可以添加代码,例如奖励。
更多信息
如果你想了解更多关于广告内容的信息,可以在这里查看 Unity 文档:docs.unity.com/ads/UnityAPI.html。
继续使用 PlayerShipBuild 脚本,我们按照以下方式声明我们的类:
public class PlayerShipBuild : MonoBehaviour, IUnityAdsListener, IUnityAdsInitializationListener
我们的脚本会使用错误线划下这些接口,因为我们还没有实现它们的方法。我们现在可以这样做,并稍后正确地填写它们。
我们不需要手动输入所需的方法,可以在 Visual Studio 中高亮显示一个接口,然后让它为我们输入方法:
- 继续使用
PlayerShipBuild脚本,用鼠标高亮显示其中一个接口。
![Figure 6.13 – 高亮显示的界面]

![Figure 6.13 – 高亮显示的界面]
- 将会出现一个下拉菜单,显示需要添加到我们类中的内容。点击显示潜在修复。
![Figure 6.14 – 显示潜在修复对话框]

![Figure 6.14 – 显示潜在修复对话框]
-
我们将看到一个方法列表(A)。点击
PlayerShipBuild类。 -
如果出现一个框要求确认更改,请点击应用。
-
对其他接口重复此过程。
-
完成后,滚动到
PlayerShipBuild脚本的底部以查看我们的额外方法。
如前所述,我们将返回这些方法,并在发生错误时添加调试信息。主要方法将是 OnUnityAdsDidFinish 方法,因为那是我们将在观看广告后向玩家奖励银行信用的地方。
我们现在将继续添加将保存 ID 的变量,以区分我们在哪个平台上运行:
-
滚动回
PlayerShipBuild脚本的顶部,并添加以下全局变量,与其他变量一起:[SerializeField] string androidGameId; [SerializeField] string iOSGameId; [SerializeField] bool testMode = true; string adId = null;
androidGameId 和 iOSGameId 将会保存我们在项目设置文件夹中设置广告服务时看到的游戏 ID。
testMode 设置为 true。在实际设备上测试模式下,你会得到一个合适的 Unity 视频测试广告。你可以测试跳过或完整观看。当你禁用测试模式进行发布时,Unity Ads 广告商的实时视频广告将出现。
adId 将保存一个字符串,该字符串将是你想要加载的广告类型的 ID。我们将在稍后对此进行更多说明。
由于我们已经通过 SerializeField 属性使新变量可访问,我们可以将这些 Android 和 iOS 游戏 ID 添加到这些变量中。
-
在我们添加更多代码之前,保存你的
PlayerShipBuild脚本并返回到编辑器。 -
在你的商店场景中,在 Hierarchy 窗口中展开 ShopManager 游戏对象。
-
也要展开 BuyingSelection。
-
选择 shop 游戏对象。
-
在 Inspector 窗口中,我们可以看到为我们的 ID 留出的空白空间。
-
因此,我们所需做的就是打开我们的 Project Settings 窗口。
-
从列表中选择 Services,然后选择 Ads。
-
复制 Android ID 并将其粘贴到 Inspector 窗口的 Android Game Id 字段中。
-
复制 iOS ID 并将其粘贴到 Inspector 窗口的 IOS Game Id 字段中。

图 6.15 – 从项目设置复制 Game id 到检查器
我们现在需要在 PlayerShipBuild 脚本中添加一个函数,该函数检查运行我们游戏的是哪种设备(iPhone/Samsung)。当我们知道运行游戏的设备时,我们可以将正确的游戏 ID 和奖励 ID 添加到我们的广告初始化器中:
-
因此,我们可以添加一个
Awake函数,并在其中调用我们尚未添加的函数:void Awake() { CheckPlatform(); } -
现在,我们可以继续声明
CheckPlatform函数;在Awake函数下一行,我们可以添加以下内容:void CheckPlatform() { string gameId = null;
我们已经声明了函数名并添加了一个默认为 null 的 gameId 变量。
-
接下来,我们将使用条件编译来区分我们在哪个平台上玩游戏,这样我们就可以连接正确的 ID:
#if UNITY_IOS { gameId = iOSGameId; adId = "Rewarded_iOS"; }
因此,如果我们使用 iPhone/iPad 设备,我们将 iOS 游戏 ID 分配给名为 gameId 的变量。我们还将 iOS 奖励分配给 adId:
#elif UNITY_ANDROID
{
gameId = androidGameId;
adId = "Rewarded_Android";
}
#endif
并且,正如你可能猜到的,我们有一个 "else if"(拼写为 elif)来检查 Android 操作系统(三星、谷歌像素设备等)。
-
最后,我们发送选定的
gameId和测试模式条件,并接收 Unity Ads 回调:Advertisement.Initialize(gameId, testMode, false, this); }更多信息
如果你想要了解更多关于条件编译的信息,请查看这个链接:
docs.unity3d.com/Manual/PlatformDependentCompilation.html。
如你所想,当我们加载 SHOP 场景时,我们的游戏会检查是否通过便携设备进行游戏。如果是通过便携设备进行游戏,它将发送正确的 ID 到 Unity。接下来,我们需要加载广告,以便如果玩家请求广告,我们可以准备好显示它。
下一步是使用一个小函数加载我们的广告。首先,我们需要检查广告是否已初始化完成:
-
要检查和加载游戏中的广告,请滚动到
Start函数并输入以下内容:StartCoroutine(WaitForAd());
一个协程将给我们等待广告初始化的能力。
-
在
Start函数之外,输入以下代码以检查我们的广告何时可以开始加载,为玩家准备:IEnumerator WaitForAd() { while (!Advertisement.isInitialized) { yield return null; } LoadAd(); }
代码将循环,直到我们的Advertisement初始化完成。一旦初始化完成,LoadAd方法将运行。
LoadAd方法将添加事件监听器,当特定任务完成时(当广告准备好查看时,如果有错误,如果广告已经开始播放)将回调到我们实现的两个接口中的方法。LoadAd方法还将做的一件事是接收发送给它的任何广告 ID 并加载特定于平台的广告。
下一个方法是我们的PlayerShipBuild脚本加载广告准备。
-
输入以下内容:
void LoadAd() { Advertisement.AddListener(this); Advertisement.Load(adId); }
重要的是要注意,每次添加监听器时,记得在不再需要时将其移除。如果创建了一个监听器,并且对象被销毁,这可能会导致问题,因为监听器可能仍然处于活动状态。此外,如果添加了另一个执行相同工作的监听器,你将加倍或三倍回调,在我们的情况下,这意味着加倍和三倍玩家的商店信用。
我们现在可以继续设置WatchAdvert函数,这将是我们在玩家按下观看广告按钮时在移动设备上触发的广告的开始:
-
仍然在
PlayerShipBuild脚本中,向下滚动到AttemptSelection方法,在那里我们已经有两个else if语句,并添加最后的else if语句以触发广告,当玩家在我们的商店场景中按下观看 广告按钮时:else if (target.name == "WATCH AD") { WatchAdvert(); }
如果这个按钮被按下,我们将运行一个名为WatchAdvert的方法。我们将在下一节讨论这个方法。
-
滚动到所有其他方法/函数之外的空间,并输入以下方法来为玩家设置广告:
void WatchAdvert() { Advertisement.Show(adId); }
WatchAdvert方法非常简单。它运行静态的Advertisement.Show方法并获取奖励 ID。
- 我们的广告几乎已经连接好了。现在我们可以向下到脚本的底部并填写我们的回调方法。
方法及其目的,不分先后,如下所示:
OnInitializationComplete()
当我们的广告服务初始化时,此方法将运行。
OnInitializationFailed(UnityAdsInitializationError error, string message)
如果广告服务初始化失败。
OnUnityAdsReady(string placementId)
当 Unity 广告准备好播放时。
OnUnityAdsDidError(string message)
加载广告时发生了一个错误。
OnUnityAdsDidStart(string placementId)
广告已经开始播放。
OnUnityAdsDidFinish(string placementId, ShowResult showResult)
广告已经播放完毕。
这些方法中的每一个都有一行代码会引发异常错误,因为我们还没有填充这些方法。这并不意味着有什么问题,它更像是一个提醒。
-
删除以下方法中包含此代码的所有代码行:
throw new System.NotImplementedException();
这将停止我们的错误。如果你想在每个这些方法中添加Debug.Log(),你可以,这样你知道这些方法是否被触发。我将只关注三个方法,其中两个用于初始化,一个用于将奖励添加到商店的银行。
-
滚动到名为
OnInitializationComplete()的方法,并在其大括号内输入以下内容:Debug.Log("Unity Ads initialization complete."); -
对于第二个方法,直接导航到
OnInitializationFailed(UnityAdsInitializationError error, string message),并在其大括号内输入以下代码:Debug.Log($"Unity Ads Initialization Failed: {error.ToString()} - {message}");
$用作插值表达式,这意味着大括号内跟随的内容也将与string中的其余部分一起记录。error将是错误的类型,而message将包含错误的描述。
信息
如果你想了解更多关于插值表达式的信息,请查看以下链接:docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated.
最后一个方法是给玩家提供的奖励。导航到OnUnityAdsDidFinish(string placementId, ShowResult showResult)方法。
如果广告被观看,我们将有三个可能的结果。可能性如下:
Finished: 完整广告已观看。
Skipped: 广告被跳过。
Failed: 广告加载失败。
这些状态将来自showResult变量。让我们输入三个可能的if语句,然后我们可以填充方法的其余部分:
-
在
OnUnityAdsDidFinish(string placementId, ShowResult showResult)方法的大括号内,输入以下内容:if (showResult == ShowResult.Finished) { // REWARD PLAYER } else if (showResult == ShowResult.Skipped) { // DO NOT REWARD PLAYER } else if (showResult == ShowResult.Failed) { Debug.LogWarning ("The ad did not finish due to an error."); } -
对于我们的游戏,我们只需要向
ShowResult.Finished语句添加内容。将// REWARD PLAYER替换为以下代码,以向玩家的银行账户奖励300积分:Debug.Log("Unity Ads Rewarded Ad Completed"); bank += 300; bankObj.GetComponentInChildren<TextMesh>().text = bank.ToString();
我们输入的第一行是一个简单的日志,表示广告已进入Finished状态。接下来,我们向现有的银行账户中添加300积分。最后,我们将值发送到bankObj的TextMesh``text值。
在OnUnityAdsDidFinish方法中的最后一个else if之后,我们通过添加两行代码来完成。第一行使用相同的奖励 ID 加载另一个广告。第二行是一个方法,用于取消选择商店中的所有按钮(我们在上一章中介绍了这个方法)。
-
因此,仍然在最后一个
else if大括号之外,输入以下两行:Advertisement.Load(placementId); TurnOffSelectionHighlights();
现在,我们可以测试我们的商店场景中的观看广告按钮,看看我们的广告模板是否可以加载:
-
保存
PlayerShipBuild脚本并返回到 Unity 编辑器。 -
确保你的构建设置指向 Android 构建。在编辑器中点击播放按钮。
-
在游戏窗口中,点击观看广告按钮。我们应该看到以下屏幕:

图 6.16 – Unity 的广告占位符屏幕
-
点击右上角的关闭按钮。
-
希望你的屏幕上的信用值已经从
600增加到900。 -
这就是添加观看广告按钮功能的结束。保存脚本。
我们的商店场景现在已经完成并完全功能化,玩家可以通过观看广告来获得信用,以便购买游戏中的物品。我们现在需要扩展PlayerSpawner脚本以支持可以添加到玩家飞船中的新物品。
扩展PlayerSpawner脚本
如果从我们的商店场景购买了一个物品,这将影响我们的游戏场景在玩家飞船加载到游戏中的行为。我们当前的PlayerSpawner脚本无法容纳shop场景的飞船,因此我们需要重新审查此脚本以更新其CreatePlayer方法:
-
在 Unity 编辑器的
PlayerSpawner脚本中(Assets/Script)。 -
在
PlayerSpawner脚本的顶部,与其他变量一起,添加一个bool值:bool upgradedShip = false;
如果在关卡中找到一个修改过的玩家飞船,upgradedShip布尔值将切换到true。
-
滚动到
PlayerSpawner脚本中的Start函数,并将以下内容作为Start函数的最后一行添加:GetComponentInChildren<Player>().enabled = true; GameManager.Instance.CameraSetup();
目前,我们的PlayerShipBuild脚本在shop场景中禁用了Player脚本,以阻止玩家在shop中射击。当我们开始我们的testLevel时,我们需要将Player脚本重新启用,以便他们可以移动和射击。我们还添加了相机设置,从近距离的商店展示到整个 z 轴上的商店展示,作为对testLevel的临时修复。
-
将
CreatePlayer方法的内文替换为以下代码以更新场景中飞船的检测://been shopping if(GameObject.Find("UpgradedShip")) { upgradedShip = true; }
我们首先需要确认PlayerSpawner脚本可以看到场景中是否购买了升级。如果已经进行了购买,修改过的玩家飞船将携带到关卡场景中。如果是这种情况,我们将upgradedShip变量设置为true。
-
继续使用
PlayerSpawner脚本,并且仍然在CreatePlayer方法中,我们添加一个if语句,实例化玩家://not shopped or died //default ship build if (!upgradedShip || GameManager.Instance.Died) { GameManager.Instance.Died = false; actorModel = Object.Instantiate(Resources.Load ("Player_Default")) as SOActorModel; playerShip = GameObject.Instantiate(actorModel. actor, this.transform.position, Quaternion. Euler(270,180,0)) as GameObject; playerShip.GetComponent<IActorTemplate>(). ActorStats(actorModel); }
在CreatePlayer方法内部继续,我们现在需要检查场景中是否有玩家飞船或者玩家是否已经死亡。如果没有升级或者玩家已经死亡,我们将使用以下代码创建一个默认玩家飞船。
在if语句内部,我们通过以下方式创建默认玩家飞船:
-
将
Died属性设置为false以停止如果玩家已经死亡时if语句的重复。 -
实例化包含我们玩家飞船所有标准属性的
Player_Default可脚本对象,并将其存储在名为actorModel的变量中。 -
接下来,我们实例化玩家的飞船,将其定位并旋转到正确的方向。
-
最后,在这个
if语句中,我们发出包含所有玩家飞船游戏对象所需属性的actorModel变量。
然而,如果我们的玩家已经购物并购买了一个或多个升级,这将进入else条件,在那里我们将找到一个名为UpgradedShip的游戏对象。我们将此游戏对象附加到我们的playerShip游戏对象变量。
-
输入以下代码以设置商店引用到
playerShip://apply the shop upgrades else { playerShip = GameObject.Find("UpgradedShip"); }
将我们的playerShip游戏对象存储为一个实例后,我们现在可以设置它使其处于正确的位置和大小,旋转等等。
-
输入以下函数以设置我们的
playerShip对象以开始游戏:playerShip.transform.rotation = Quaternion.Euler(0,180,0); playerShip.transform.localScale = new Vector3(60,60,60); playerShip.GetComponentInChildren<ParticleSystem> ().transform.localScale = new Vector3(25,25,25); playerShip.name = "Player"; playerShip.transform.SetParent(this.transform); playerShip.transform.position = Vector3.zero; GameManager.Instance.CameraSetup(); } }
然后,我们继续编写PlayerSpawner脚本的最后一段代码,其中我们的玩家飞船被设置好以准备开始。请注意,即使玩家购买了升级,这也不会在获取玩家飞船引用方面引起任何问题。
-
设置旋转,使玩家的飞船面向正确的方向。
-
正确缩放玩家的飞船。
-
关闭
Player脚本,以便在执行开场动画时玩家无法控制飞船。 -
将玩家的飞船命名为
Player。 -
最后,将玩家的飞船设置为
playerSpawner游戏对象的子对象,因为它属于playerSpawner游戏对象。 -
返回到
GameManager脚本,滚动到void CameraSetup()并将它的访问修饰符更改为public。因此它读作public void CameraSetup()。 -
保存脚本。
我们已经更新了玩家的飞船,使其可以创建为默认飞船或从商店场景中定制的飞船。我们还使其意识到PlayerTransition脚本,以便当玩家的飞船被创建时,它不会卡在屏幕边界内,或者玩家不会处于无法控制飞船的位置,直到其开场动画完成。
最后,我们现在需要创建并添加我们的b. Shot武器资产到游戏中。大多数脚本编写工作已经完成;它只需要附加到正确的组件上。
为了给我们的b. Shot预制武器赋予行为,我们需要做以下事情:
-
在
Assets/Resources中,选择b. Shot。 -
现在,在
BShot Component中,直到它出现在下拉列表中,然后选择它。
我们现在需要将我们的bShotComponent应用到脚本上。
- 点击
player_BshotBullet旁边的那个小、圆形的遥控圈,如下面的截图所示:
![Figure 6.17 – B Shot Component script with player_BshotBullet game object]
![img/Figure_6.17_B18381.jpg]
图 6.17 – B 射击组件脚本与player_BshotBullet游戏对象
我们的b. Shot武器现在将发射。接下来,我们需要让子弹移动,遵循与我们在第二章中相同的流程,添加和操作对象,在那里我们让第一个玩家子弹发射并穿越屏幕。这意味着我们可以使用我们已编写的脚本并将其附加到player_BshotBullet预制件上。
要附加并自定义player_BshotBullet预制件,我们需要执行以下操作:
-
在
Assets/Prefab/Player中,选择player_BshotBullet。 -
在
PlayerBullet中找到它,直到它在下拉列表中显示,然后选择它。 -
回到
Assets/ScriptableObject。 -
在项目窗口右侧的空白区域右键单击,然后选择创建 | 创建演员。
-
在
bShotBullet中命名新文件并选择它。 -
在检查器窗口中给它以下值:

图 6.18 – ShotBullet Scriptable Object 及其属性
我们正在接近本章的结尾。现在是检查一切以查看它如何为我们发挥作用的时候了:
-
保存所有打开的脚本。
-
保存商店场景。
-
在 Unity 编辑器中点击播放模式。
-
尝试购买所有三个飞船升级(你需要观看几个 Unity 通知广告以获得它们)。
-
点击开始按钮,你应该会看到一个类似于以下屏幕的界面,我们的飞船持有所有三个升级:

图 6.19 – 我们带有所有升级的玩家飞船
我们的玩家现在全副武装!如果玩家被击中,他们将失去护盾,飞船的前部覆盖物将消失。如果飞船使用火力升级,它将消灭所有敌人。目前原子弹尚未编程执行任何操作。
如果你在本节的最后遇到了任何问题,你可以查看官方 Unity 指南,其中还包含有关广告和奖励的其他信息,如果你感兴趣的话(unityads.unity3d.com/help/unity/integration-guide-unity)。或者将我们的脚本和场景与本章开头提供的下载项目文件的链接中的Chapter6的Complete文件夹进行比较。
恭喜你,你已经到达了本章的结尾,也到达了商店场景的结尾!希望你觉得这很有益,并能欣赏 Unity 如何热衷于鼓励开发者寻找从游戏中通过广告赚钱的替代方法,而不是简单地销售他们开发的游戏。我们现在将总结本章涵盖的内容,并探讨我们将如何进一步构建我们的游戏。
摘要
在本章中,我们创建了一个可以与之交互的场景,以便通过应用内购买来修改我们的玩家,同时我们还可以通过在 iPhone 或 Android 手机上观看广告来获得更多游戏内积分,从而购买更多物品以进一步升级玩家。我们将在本书的附录部分查看如何在移动设备上构建游戏,以及如何在 PC/macOS 平台上隐藏广告按钮。
最后,我们将所有购买的物品都带到了游戏中,因此玩家的游戏玩法因修改而改变。
如本章中提到的几次,从艺术角度来看,场景已经为我们准备好了。这样做的原因是让你体验射线投射对象,并理解这是在场景中交互的另一种方式。但如果我们在上面玩游戏的平台是 iPad 呢?与 iPhone 或 Android 手机的信封形状相比,iPad 更接近方形。如果是这种情况,我们的游戏摄像头会裁剪掉商店场景的一部分。
你也可以考虑在编码中使用诸如.Find之类的功能的一种更有效的方法。如果这是你的担忧,请不要担心——我们在第九章中解决了这些问题,即创建 2D 商店界面和游戏内 HUD,在那里我们实现了 Unity 自己的事件系统。然而,现在,让我们看看下一章我们将要涵盖的内容。
在下一章中,我们将把所有场景连接起来,创建我们所说的游戏循环。这将帮助我们理解整个游戏布局。
第七章:第七章:创建游戏循环和模拟测试
在上一章中,我们从testLevel场景(我们在这里控制玩家飞船)移动到shop场景(购买和校准玩家的飞船)。在本章中,我们将遵循类似趋势,扩展到Scene文件夹中的其他游戏场景(位于Assets文件夹中)。
作为场景管理的一部分,我们玩的所有游戏都有一种称为“游戏循环”的东西——如果你不熟悉这个术语,它基本上意味着我们的游戏将有多条替代路线。每条路线将加载特定的场景。我们需要在每个游戏阶段为每种结果做好准备。
最终,所有游戏循环都会回到接近开始的地方。以下图像显示了在本章结束时我们的游戏循环将看起来是什么样子:

图 7.1 – Killer Wave 的游戏循环
仍然参考游戏循环图像,每个矩形框中的每个名称都代表 Unity 中我们Scene文件夹内的一个场景。每个场景的流程都沿着一个整体方向进行,从顶部的BOOTUP开始。这是游戏首次启动时。流程将穿过每个场景,直到玩家击败所有三个级别或死亡。无论如何,他们最终都会到达GAMEOVER场景,然后循环回到TITLE场景以创建循环。
到本章结束时,我们将能够从bootUp场景运行我们的游戏,它将自动移动到title场景。从那里,玩家将按下射击按钮或轻触屏幕,然后加载shop场景,这是可以进行购买的地方。
一旦玩家按下shop场景,第一级(level1)将开始。如果玩家死亡,该级别将重新启动,而如果玩家完成该级别,他们将继续到下一级。
最终的结果将是,如果玩家死亡三次,他们将被带回到title场景,而如果玩家完成level3场景,则游戏结束,玩家将被带到gameOver场景。
最后,我们将涵盖一些与我们迄今为止所涵盖内容相关的模拟测试问题。
在本章中,我们将涵盖以下主题:
-
转换我们的玩家飞船
-
扩展我们的
ScenesManager脚本 -
准备循环我们的游戏
-
模拟测试
本章涵盖的核心考试技能
以下是在本章中将涵盖的核心考试技能:
编程核心交互:
-
实现和配置游戏对象行为和物理
-
实现和配置摄像机视图和移动
在艺术管道中工作:
-
理解材质、纹理和着色器,并编写与 Unity 渲染 API 交互的脚本
-
理解 2D 和 3D 动画,并编写与 Unity 动画 API 交互的脚本
场景和环境设计的编程:
-
识别实现游戏对象实例化、销毁和管理的方法。
-
识别用于构建模块化、可读性和可重用性脚本的技巧。
技术要求。
本章的项目内容可以在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_07找到。
您可以下载每个章节的项目文件的全部内容,链接为github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition。
本章的所有内容都包含在本章的unitypackage文件中,包括一个Complete文件夹,其中包含本章我们将执行的所有工作。
查看以下视频以查看代码执行情况:bit.ly/37Rie3B。
转换我们的玩家飞船。
目前,我们的关卡只能在玩家死亡、关卡重新开始或玩家失去所有三个生命时完成。只有在这种情况下,我们才会被带到游戏结束屏幕。我们现在需要开始思考玩家如何开始和结束关卡。目前,玩家只是在关卡开始时出现。
在本节中,我们将编写一些代码,将我们的玩家动画化到场景中,并在关卡完成时,我们将让玩家飞船退出摄像机视图。
因此,让我们像创建所有其他脚本一样创建一个脚本(第二章**,添加和管理对象,如果您需要参考)。将脚本命名为PlayerTransition并确保我们在 Unity 编辑器的项目窗口中的Script文件夹中有该文件。
现在,我们需要将PlayerTransition脚本附加到我们的player_ship预制体上:
-
从项目窗口中加载
testLevel场景到Assets/Scene。 -
然后,导航到
Assets/Prefab/Player文件夹,并选择player_ship预制体。 -
最后,将
PlayerTransition脚本拖放到player_ship的PlayerTransition组件的空区域,player_ship的PlayerTransition组件将开始动画化shop场景中的player_ship。
现在我们已经创建了脚本并将其附加,我们可以进入它并开始设置以下代码:
-
向我们的
PlayerTransition脚本添加变量。 -
向我们的
PlayerTransition脚本添加方法/函数。 -
添加
if语句检查。 -
向
PlayerMovement IEnumerator添加内容。 -
将玩家飞船移出屏幕。
让我们来看看。
向我们的PlayerTransition脚本添加变量。
在本节中,我们将从设置我们的PlayerTransition脚本开始。我们将通过添加全局变量来实现这一点,以便可以使用这些变量来定位玩家飞船。
要开始添加我们的全局变量,请按照以下步骤操作:
-
打开我们新创建的
PlayerTransition脚本。 -
在脚本顶部,确保我们添加了以下库:
using UnityEngine;using System.Collections;
默认情况下,我们的脚本应该自动命名,并与其默认继承的 MonoBehaviour 一起使用,因为这是关于 Unity 编辑器和其他功能的要求。我们将使用 System.Collections 库来执行我们的 StartCoroutine。没有这个库,我们无法创建协程;我们将在编写代码时进一步解释这一点。
-
检查/输入以下代码用于我们的
PlayerTransition脚本,它保留了脚本的默认名称和MonoBeaviour继承以添加功能:public class PlayerTransition : MonoBehaviour { -
在
PlayerTransition类中,输入以下全局Vector3变量:Vector3 transitionToEnd = new Vector3(-100,0,0); Vector3 transitionToCompleteGame = new Vector3(7000,0,0); Vector3 readyPos = new Vector3(900,0,0); Vector3 startPos;
startPos 和 readyPos 变量用于测量我们的玩家飞船当前位置与我们希望其前往的位置之间的距离。
提示
在这一点上,请确保 _Player 游戏对象的 Transform Position 属性值在 Inspector 窗口中设置为 X、Y 和 Z 轴上的零。否则,玩家飞船在进入关卡时可能会动画到错误的位置。
transitionToEnd 变量将被用作我们希望在关卡开始时我们的玩家游戏对象飞船要前往的坐标,以及当玩家的飞船即将离开关卡时。transitionToCompleteGame 仅在玩家完成第三级和最终级时使用,用于改变玩家的结束动画。
-
继续在我们的
PlayerTransition脚本中输入以下float变量:float distCovered; float journeyLength;
distCovered 将持有时间数据,这些数据将被用于稍后测量两个 Vector3 点(我们将在制作 PlayerMovement 为 IEnumerator 时详细讨论这一点)。
journeyLength 将持有之前提到的两个 Vector3 点(startPos 和 readyPos)之间的距离。
-
最后的一组变量是要添加到我们的
PlayerTransition脚本中的bool。bool levelStarted = true; bool speedOff = false; bool levelEnds = false; bool gameCompleted = false; public bool LevelEnds { get {return levelEnds;} set {levelEnds = value;} } public bool GameCompleted { get {return gameCompleted;} set {gameCompleted = value;} }
levelStarted 是唯一被设置为 true 的 bool,因为它确认了关卡已经开始,并且只有在玩家动画过渡完成后才会被设置为 false。speedOff 将在我们要让玩家的飞船离开关卡时设置为 true。
当关卡结束时,levelEnds 被设置为 true,此时玩家飞船将移动到其出口位置。最后一个 bool 用于整个游戏完成时。这用于改变结束动画。这两个属性用于从脚本外部访问 levelEnds 和 gameCompleted 变量。
那就是添加到我们脚本中的变量。现在,让我们继续到 PlayerTransition 的方法和函数。
向我们的 PlayerTransition 脚本添加方法/函数
在我们继续 PlayerTransition 脚本的过程中,我们将添加 Unity 的 Start 函数并创建我们自己的 Distance 方法来定位玩家的飞船到正确的位置:
-
从
Start函数开始,继续为我们的PlayerTransition脚本输入以下代码:void Start() { this.transform.localPosition = Vector3.zero; startPos = transform.position; Distance(); }
Start函数在脚本启用后立即被调用。在这个函数中,我们将玩家飞船的位置重置为其父游戏对象,即PlayerSpawner游戏对象。
我们将把玩家飞船的初始世界空间位置分配给我们在此节中较早创建的一个向量(startPos)。我们将在Distance方法中使用这个向量,我们将在下一节中讨论它。
-
在
PlayerTransition类中输入Distance方法及其内容:void Distance() { journeyLength = Vector3.Distance(startPos, readyPos); }
Vector3.Distance是 Unity 的一个现成函数,它将测量两个向量点之间的距离,并以float形式给出答案,我们将将其存储在journeyLength中。这样做的原因是我们将想知道玩家飞船当前位置和它需要到达的位置之间的距离(我们将在本章稍后讨论)。
在下一节中,我们将进入 Unity 的Update函数,检查关卡何时结束,以便我们可以将玩家飞船移出屏幕。
添加 if 语句检查
在本节中,我们将利用 Unity 的帧更新函数Update,以便我们可以运行检查以查看游戏在关卡中的状态。
在我们的Update函数中,我们将有三个if语句。levelStarted是从我们在此节较早介绍的一个布尔变量中来的,它已经设置为true。因此,这个if语句将立即被调用。让我们看看:
-
让我们从
PlayerTransition脚本中的Update函数的第一个if语句开始:void Update() { if (levelStarted) { PlayerMovement(transitionToEnd, 10); }
在第一个if语句中有一个名为PlayerMovement的方法,它也接受两个参数。关于这个方法的作用,我们将在覆盖完整个Update函数后对其进行回顾。
现在,让我们继续处理Update函数中的第二个if语句。
这个if语句检查levelEnds变量是否为true,如您所记得,我们将其默认设置为false。这个布尔值在PlayerTransition类外部访问,我们将在稍后讨论,但就目前而言,我们只需要知道它在关卡结束时变为true。
在if语句内部,有几行代码为玩家飞船准备开始结束关卡,首先是通过将Player脚本的enabled布尔设置设置为false来禁用Player脚本。这将使玩家失去控制,以便我们可以将玩家飞船动画化到关卡结束的位置。
接下来,我们禁用玩家飞船的SphereCollider,这样如果敌人或其子弹与玩家飞船接触,在准备结束关卡时,它不会摧毁飞船。
-
在
PlayerTransitionUpdate函数内部输入第二个if语句:if (levelEnds) { GetComponent<Player>().enabled = false; GetComponent<SphereCollider>().enabled = false; Distance(); PlayerMovement(transitionToEnd, 200); }
然后,我们使用Distance方法来测量玩家飞船在关卡开始时的位置和它需要到达的位置之间的距离。
最后,在if语句中,我们有我们之前提到的方法,唯一的区别是参数值设置为200。这些值将在第四个if语句之后的这个Update函数中解释。
当我们仍然在Update函数内部时,我们可以进入覆盖玩家完成第三和最后一个级别的if语句:
if (gameCompleted) { GetComponent<Player>().enabled = false; GetComponent<SphereCollider>().enabled = false; PlayerMovement(transitionToCompleteGame, 200); }
如果gameCompleted bool为true,我们将进入if语句的条件。在内部,我们关闭Player脚本以禁用玩家的控制。第二行禁用玩家的碰撞器,以避免与任何与敌人相关的游戏对象发生碰撞,而第三行使玩家飞船从当前位置移动到transitionToCompleteGame的值。
-
进入我们的
PlayerTransitionUpdate函数中的第四个if语句:if (speedOff) { Invoke("SpeedOff",1f); }}
在第四个if语句中,我们运行一个检查以查看speedOff bool是否持有true的值。如果是这样,我们运行 Unity 自己的Invoke函数,该函数将SpeedOff方法的执行延迟 1 秒。
更多信息
更多关于Invoke的信息可以在 Unity 脚本参考网站上找到:docs.unity3d.com/ScriptReference/MonoBehaviour.Invoke.html。
在下一节中,我们将编写一些代码,以便将玩家从他们所在的位置移动到他们需要到达的位置。需要实现这种情况的两个案例如下:
-
当玩家开始游戏时,我们将动画化他们进入场景。
-
当玩家完成关卡后,他们需要移动到一个位置以离开关卡。
我们将介绍两个新的 Unity 函数(Mathf.Round和Vector3.Lerp)。
向PlayerMovement方法添加内容
PlayerMovement方法负责在屏幕近中心处动画化我们的玩家飞船,以便它可以从一个位置开始并退出关卡。
让我们更详细地了解这一点:
-
为我们的
PlayerMovement方法及其两个参数输入以下代码:void PlayerMovement(Vector3 point, float transitionSpeed) {
如前所述,我们的PlayerMovement方法接受两个参数:一个带有参考名称point的Vector3和一个带有参考名称transitionSpeed的float。正如你可以想象的那样,transitionSpeed是玩家飞船从一个点到另一个点移动的速度。
如果我们回溯到point的值,它来自一个我们已经在脚本开头初始化的变量,即transitionToStart,其值为(-100,0,0)的Vector3。
因此,实际上 transitionToStart 和 point 是相同的——它们只是名称不同,为了保持它们的引用独立。无论如何,回到 point,这个值是我们玩家飞船的位置。以下截图显示了我们的玩家飞船,位置为 -100,0,0:

图 7.2 – 玩家飞船的位置
因此,当关卡开始时,我们的玩家飞船将位于屏幕的左侧边缘,并且会动画过渡到之前截图中所标记的位置。
继续使用 PlayerMovement 方法,我们从一个 if 语句开始,该语句检查一系列条件是否满足。
-
输入以下
if语句及其四个条件:if (Mathf.Round(transform.localPosition.x) >= readyPos.x -5 && Mathf.Round(transform.localPosition.x) <= readyPos.x +5 && Mathf.Round(transform.localPosition.y) >= -5f && Mathf.Round(transform.localPosition.y) <= +5f) {
在之前的代码中,我们已经四次检查了玩家是否处于正确的位置,然后再执行其余的代码。每一行代码都会检查以下内容:
-
如果玩家飞船的 X 位置大于或等于存储在
readyPos变量 X 位置的值,减去 5。 -
如果玩家飞船的 X 位置小于或等于存储在
readyPos变量 X 位置的值,加上 5。 -
如果玩家飞船的 Y 位置大于或等于存储在
readyPos变量 Y 位置的值,减去 5。 -
如果玩家飞船的 Y 位置小于或等于存储在
readyPos变量 Y 位置的值,加上 5。
-
仍然在
PlayerMovement方法中,并且在之前的if语句内,输入以下两个if语句:if (levelEnds) { levelEnds = false; speedOff = true; } if (levelStarted) { levelStarted = false; distCovered = 0; GetComponent<Player>().enabled = true; } }
在之前的代码块中,我们有两个 if 语句(levelEnds 和 levelStarted),它们检查每个 bool 条件是否为 true。让我们来看看它们的内容:
-
levelEnds:如果levelEnds变为true,我们将levelEnds的bool设置为false,并将speedOff的bool设置为true。 -
levelStarted:如果levelStarted被赋予true的值,我们将levelStarted的bool设置为false,将distCovered的float设置为0,并将Player脚本设置为true。
-
最后,在我们的
PlayerMovement方法中,输入以下位于主if语句之外的else条件:else { distCovered += Time.deltaTime * transitionSpeed; float fractionOfJourney = distCovered / journeyLength; transform.position = Vector3.Lerp(transform. position, point, fractionOfJourney); } }
查看之前的 else 条件代码块,我们添加时间并将其乘以 transitionSpeed,如您所记得的,这是此方法接受的两个参数之一。确保此 else 语句与 Mathf.Round 语句相关,而不是检查两个 bool 的语句。
然后,我们将 distCovered 变量除以 journeyLength 变量,如您所记得的,这是两点之间的测量值。我们将除法的结果存储在一个名为 fractionOfJourney 的 float 变量中。
在此 else 条件中我们最后做的事情是使用 Unity 的预制函数之一 Lerp,它将我们的玩家飞船在两点之间进行线性插值。Lerp 接受三个参数:点 A、点 B 和它将在两点之间移动的时间比例。transform.position 是我们的玩家飞船,第二个是 Vector3 点,这是我们带到 IEnumerator 中的另一个变量,第三个是活动的 float fractionOfJourney。
信息
还可以使用 Material.Lerp 在一段时间内渐变颜色。有关将一种颜色渐变为另一种颜色的更多信息,请参阅docs.unity3d.com/ScriptReference/Material.Lerp.html。
我们现在需要在 PlayerSpawner 脚本中添加一行代码,在玩家离开商店场景后启用 PlayerTransition 脚本。如本章前面所述,如果 PlayerTransition 在商店场景中保持开启状态,player_ship 将在屏幕上动画化。
因此,为了在 level1 场景的开始时开启 PlayerTransition 脚本,我们需要做以下操作:
-
在
Assets/Script中打开PlayerSpawner脚本。 -
滚动到以下代码行:
playerShip.transform.position = Vector3.zero; -
在此之后输入以下代码行:
playerShip.GetComponent<PlayerTransition>().enabled = true;
这行代码将使我们的玩家飞船动画化进入 level1 场景。
我们需要在 PlayerSpawner 脚本中做的最后一个更改是移除在 PlayerSpawner Start 函数中启用 Player 脚本的能力,我们将在 ScenesManager 脚本中启用它。
-
在
PlayerSpawner脚本中,从Start函数中删除以下行:GetComponentInChildren<Player>().enabled = true; -
保存
PlayerSpawner脚本。
现在,让我们继续到最后一段代码,我们将在这里移动玩家飞船,使其在关卡结束时离开屏幕。
将玩家飞船移出屏幕
我们需要在 PlayerTransition 脚本中覆盖的最后一个方法是 SpeedOff 方法。此方法简单地将我们的玩家飞船喷射出屏幕,当关卡完成时。让我们看看:
-
在我们的
PlayerTransition脚本中输入以下代码:void SpeedOff() { transform.Translate(Vector3.left * Time.deltaTime*800); }
此代码块使用 Unity 的预制 Translate 函数,它接受 Vector3.left 乘以时间,其中 800 用于使玩家飞船移动得更快。
- 保存脚本。
这就是 PlayerTransition 脚本的结束。现在,我们的游戏为玩家飞船添加了开场和结尾。最初,我们的玩家将在关卡开始时出现,当它被标记为完成时,将加载下一关卡。我们还技术性地覆盖了三个新函数,如下所示:
-
Vector3.Distance,用于测量两个Vector3点之间的距离 -
Vector3.Lerp,用于移动玩家飞船,在两个Vector3点之间平滑移动 -
MathF.Round,用于四舍五入一个数字
我们将这些新技能结合起来,使我们的玩家飞船进入位置以开始关卡,完成后,无论玩家在屏幕上的位置如何,我们都将他们移动到正确的位置。最后,我们的玩家从屏幕上飞走。
在下一节中,我们将重新访问ScenesManager脚本并应用一些代码,以便有一个时间限制,倒计时到关卡结束。
扩展ScenesManager脚本
在本节中,我们将使我们的ScenesManager脚本识别scenes文件夹(Assets/Scene)中的 2 级和 3 级关卡。然后我们将这些关卡添加到游戏循环中。此外,我们还将为每个关卡添加一个游戏计时器。当计时器达到其限制时,我们可以通过播放一个动画来触发玩家离开关卡。最后,我们将添加一些常见的方法来将游戏推进到下一级。
让我们从打开ScenesManager脚本(Assets/Script/Scenesmanager.cs)并添加一些变量开始,以帮助我们讨论的内容。按照以下步骤操作:
-
在
ScenesManager脚本顶部添加以下变量:float gameTimer = 0; float[] endLevelTimer = {30,30,45}; int currentSceneNumber = 0; bool gameEnding = false;
gameTimer变量计时器将用作我们的当前计数器,以计时关卡剩余的时间直到结束。以下变量是一个数组,它保存了每个关卡结束前的时间。因此,我们总共有三个关卡,但问题是,我们希望每个关卡持续多长时间?我们需要输入一个表示关卡结束前秒数的值,所以我选择了 1 级和 2 级的30秒。然而,3 级关卡将持续45秒。这是因为我们将在这个章节中构建一个特殊的关卡,NavMesh, Timeline, and a Mock Test。我们将在这个章节中详细介绍这一点。
如您所想象,currentSceneNumber将保存表示玩家当前所在场景的数字。最后,我们有gameEnding布尔值,它将用于触发玩家飞船关卡动画的结束。我们将在本节稍后更详细地介绍这些变量,让我们从currentSceneNumber开始。
在我们刚刚设置的全球变量之后,让我们确保ScenesManager脚本在整个游戏过程中始终知道玩家所在的是哪个场景。这将帮助我们的代码知道玩家当前所在场景以及他们将要进入的场景。
-
添加
Update函数,它将在每一帧被调用以检查我们处于哪个场景。请在ScenesManager脚本中输入以下代码:void Update() { if (currentSceneNumber != SceneManager.GetActiveScene().buildIndex) { currentSceneNumber = SceneManager.GetActiveScene().buildIndex; GetScene(); } GameTimer(); }
在Update函数内部,我们使用一个if条件来检查currentSceneNumber变量是否不等于我们从活动场景中获取的buildIndex。
如果不相等,我们将使用当前场景的buildIndex更新currentSceneNumber,然后调用GetScene方法。GetScene方法是一个小的方法,现在就介绍它比以后介绍更好,因为它与我们刚刚讨论的所有内容都相关。
在 GetScene 方法中,有一行代码用于更新场景的变量。这是一个来自 Scenes 枚举的实例,它包含我们游戏中每个场景的名称。此外,GetScene 方法中的代码将 currentSceneNumber 强制转换为枚举,这就是为什么 Scenes 类型放在括号中的原因。更多关于强制转换的信息可以在 docs.microsoft.com/en-us/dotnet/csharp/programming-guide/types/casting-and-type-conversions 找到。
-
为我们的
ScenesManager脚本输入以下代码:void GetScene() { scenes = (Scenes)currentSceneNumber; }
我们可以在 ScenesManager 类中的任何地方放置 GetScene 方法,只要它不在另一个方法内部。
回到 Update 函数,在调用 GetScene 方法之后,我们关闭 if 条件的括号。在关闭 Update 函数之前,我们最后要做的事情是运行 GameTimer 方法,这个方法将跟踪我们游戏的时间,并设置一些基本方法,这些方法将开始、重置和结束我们的游戏关卡。
在接下来的部分,我们将涵盖以下主题:
-
为每个游戏关卡添加计时器。当计时器结束时,这会通知玩家已经完成了关卡。
-
让
ScenesManager脚本知道关卡完成后要做什么;也就是说,加载关卡,哪个关卡,等等。
让我们开始吧。
添加游戏关卡计时器
在 ScenesManager 脚本中,我们将设置一个负责确认关卡结束的方法。GameTimer 方法的作用是为 gameTimer 变量添加时间,并检查它是否达到了其限制,这取决于它比较的 endLevelTimer。最后,如果游戏被触发结束玩家飞船的动画,它将被设置为开始,并在 4 秒后加载下一关卡。
在你的 ScenesManager 脚本仍然打开的情况下,将以下方法添加到你的代码中:
void GameTimer() { switch (scenes) { case Scenes.level1 : case Scenes.level2 : case Scenes.level3 : { if (gameTimer < endLevelTimer[currentSceneNumber-3]) { //if level has not completed gameTimer += Time.deltaTime; } else { //if level is completed if (!gameEnding) { gameEnding = true; if (SceneManager.GetActiveScene().name != "level3") { GameObject.FindGameObjectWithTag("Player"). GetComponent <PlayerTransition>().LevelEnds = true; } else { GameObject.FindGameObjectWithTag("Player"). GetComponent <PlayerTransition> ().GameCompleted = true; } Invoke("NextLevel",4); } } break; } } }
在 GameTimer 方法中,我们运行一个包含 scenes 实例的 switch 语句,该实例将包含每个级别的所有枚举名称。我们对三个可能的案例进行检查:level1、level2 和 level3。如果 scenes 实例设置为这三个可能性中的任何一个,我们就会进入一个 if 条件,然后比较 gameTimer 变量是否小于 endLevelTimer 数组已设置的限制。
我们只需要知道关卡 1、2 和 3 的构建索引号。因此,为了避免前三个场景,我们必须减去 3。
以下截图显示了 构建设置 窗口(文件 | 构建设置),其中包含你的项目右侧的场景及其构建编号:


Figure 7.3 – Build Settings – scene order
如果 gameTimer 小于 levelTimer,我们将使用 Unity 为我们预先制作的 Time.deltaTime 固定函数继续增加 gameTimer。有关 Time.deltaTime 的更多信息,请参阅此处:docs.unity3d.com/ScriptReference/Time-deltaTime.html。
如果 gameTimer 等于或大于 levelTimer,我们将进入 else 条件,该条件检查 gameEnding bool 的 if 语句条件是否为 false。如果条件为 false,我们将进入 if 语句的内容,首先将 gameEnding bool 设置为 true。这将阻止 if 语句在 Update 函数的帧周期中重复。
最后的 if 语句检查游戏处于哪个关卡。如果不是 "level3",我们在 PlayerTransition 脚本中将 LevelEnds 属性设置为 true。否则,我们必须已经完成了游戏。因此,在 else 条件中,我们在 PlayerTransition 脚本中将 GameComplete 属性设置为 true。
在本节中,我们在 ScenesManager 脚本中创建了一个方法,使我们的游戏知道每个关卡将持续多长时间,然后将其视为已完成。
我们现在将继续在 ScenesManager 脚本中添加方法,这些方法将在 GameTimer 方法触发时开始、重置并将玩家移动到下一个关卡。
开始、重置和跳过关卡
ScenesManager 将负责开始一个关卡,当玩家死亡时重置它,并在当前关卡完成后移动到下一个关卡。幸运的是,由于 Unity 的 SceneManagement 库,这些工作需要的工作量很小。
让我们从回顾我们已经开始的 ResetScene 方法开始,但现在,我们将进一步简化它:
-
在我们的
ScenesManager脚本中,用以下代码替换ResetScene方法中的内容:public void ResetScene() { gameTimer = 0; SceneManager.LoadScene(GameManager.currentScene); }
在 ResetScene 方法内部,我们将 gameTimer 变量重置为零,然后将其参数从当前的 SceneManager.LoadScene buildIndex 替换为 GameManager.currentScene,这是我们回到 第三章,管理脚本和进行模拟测试 时编写的。这基本上就是将当前构建索引作为一个 static 整数保持下来,以便任何脚本都可以访问它。
更新 ResetScene 后,我们现在可以继续进行下一个方法,这个方法与我们刚刚做的非常相似,但它与 ResetScene 分开,以便支持我们代码的扩展。
当玩家完成一个关卡时,NextLevel 方法会运行,这将重置 gameTimer 变量。gameTimer bool 将被设置为 false,并使用相同的 SceneManager.LoadScene 命令将 GameManager currentScene 整数增加 1。
-
在
ScenesManager脚本中输入以下方法:void NextLevel() { gameEnding = false; gameTimer = 0; SceneManager.LoadScene(GameManager.currentScene+1); }
我们在ScenesManager脚本中需要更改的最后一个方法是BeginGame方法,当玩家在shop场景中并按下"START"按钮时将被调用。
-
为我们的
ScenesManager脚本输入以下代码:public void BeginGame(int gameLevel) { SceneManager.LoadScene(gameLevel); }}
BeginGame方法将接受一个名为gameLevel的整数参数。在这个方法内部,我们有之前已经使用过的SceneManager.LoadScene,但这次,它将加载我们提供的gameLevel整数。
- 保存脚本。
如果你记得,在上一章中,我们将PlayerSpawner脚本临时设置为调用GameManager.Instance.CameraSetup();
这个调用不再需要,现在可以删除。让我们删除它
-
在
Assets/Script。 -
加载
PlayerSpawner脚本并从Start函数中删除GameManager.Instance.CameraSetup();这一行。 -
保存脚本。
由于我们将BeginGame方法改为现在接受一个参数,我们必须更新我们的PlayerShipBuild脚本,该脚本有一个StartGame方法,它运行BeginGame方法,目前没有参数值。为了更新PlayerShipBuild的StartGame方法,我们需要做以下几步:
-
在 Unity 编辑器中,导航到项目窗口中的
Assets/Script/PlayerShipBuild.cs文件夹并打开它。 -
滚动到
StartGame方法并找到以下代码行:UnityEngine.SceneManagement.SceneManager.LoadScene("testLevel"); -
现在,将前面的代码行改为以下内容:
GameManager.Instance.GetComponent<ScenesManager> ().BeginGame(GameManager.gameLevelScene);
这个代码更改现在将直接调用level1场景。
有了这些,我们就到达了本节的结尾。到目前为止,我们已经涵盖了以下内容:
-
我们的游戏现在知道一个级别需要多长时间才能被归类为完成。
-
ScenesManager脚本现在可以调用将启动、重置并将玩家移动到下一级的方法。
我们的大多数代码都是使用switch语句和enum来调用,当场景需要改变时使用。为了加载场景本身,我们使用了 Unity 自带的SceneManager类,这是在 Unity 项目中加载任何场景的基础。
在下一节中,我们将准备项目中不是游戏级别的其余场景(bootUp场景、title场景和gameOver场景)。
准备循环我们的游戏
在本节中,我们将从testLevel场景移开,并介绍三个其他级别(level1、level2和level3),以演示游戏循环。
到本节结束时,我们的游戏循环将完成。我们将能够从bootUp场景开始我们的游戏。从那里,我们将能够通过每个场景进行进度。
让我们先从 Unity 编辑器中删除占位符级别开始。转到Assets/Scene位置。按照以下步骤操作:
-
确保你的
player_ship已保存(testLevel场景)。然后,删除level1、level2和level3。 -
选择
testLevel,在键盘上按住左 Ctrl (Mac 上的 Command)键,然后按D两次。我们现在应该有三个testLevel实例。 -
将
testLevel重命名为level1。 -
将
testLevel 1重命名为level2。 -
将
testLevel 2重命名为level3。
我们现在需要检查 构建设置 窗口以查看场景的顺序。
- 在 Unity 编辑器的顶部,点击 文件 | 构建设置。
我们应该看到的顺序如下面的截图所示。如果不是这样,请通过在 构建设置 窗口中点击并拖动场景以及选择并删除列表中的任何额外场景来选择并移动场景到正确的位置:

图 7.4 – 构建设置 – 完整场景顺序
我们已经将第一个关卡复制了两次,以测试我们的关卡是否可以完成并继续前进。接下来,我们将回到项目列表中的第一个场景,并设置它以便它可以像启动场景一样运行。
由于我们已删除 testLevel 场景,我们需要更新 GameManager 脚本中的 LightandCameraSetup 方法,以保持其 Switch 语句与我们需要照亮的关卡同步,以及设置我们的相机。
为了确保我们的相机和灯光为每个场景正确工作,我们需要执行以下操作:
- 在
Assets/Script文件夹中。
双击 GameManager 脚本。
-
滚动到
LightandCameraSetup方法的具体内容,并确保每个案例编号遵循以下模式:case 3 : case 4 : case 5:
每个案例代表玩家将要玩的游戏关卡。
- 保存脚本。
在接下来的几节中,我们将为每个非关卡场景(基本但信息丰富)自定义一个占位符外观。这些场景如下:
-
bootUp -
title -
gameOver
这些场景也将需要基本的编码,以便玩家可以按按钮继续,或者发出一个计时器。这个计时器将倒计时,直到加载下一个场景。
设置启动场景
当我们运行游戏时,通常游戏不会立即开始 - 通常有一个启动屏幕来显示谁开发了/发布了游戏。有时,它用作加载屏幕,但对我们来说,它将用于启动我们的游戏。在本节中,我们将移除典型的 Unity 天空背景,并用一个中性灰色背景替换它,并在背景上显示一个文本标题,说明已加载哪个屏幕。
让我们开始,并在 Unity 编辑器中打开 bootUp 场景:
-
通过访问
Assets/Scene中的level1场景。 -
如果尚未打开,请双击
prefab文件夹。 -
接下来,双击从
Assets/Scene中打开的bootUp场景文件。 -
将
Assets/Prefab中的GameManager预制体拖放到 层次结构 窗口中。 -
在 层次结构 窗口中创建一个空的游戏对象。如果您忘记了如何做,请参阅 第二章,添加和操作对象。
-
将新创建的游戏对象命名为
BootUpText。 -
如前所述,创建另一个空的游戏对象并命名为
BootUpComponent。
以下截图显示了 层次结构 窗口左侧的组件。这些组件如下:
-
主相机
-
方向光
-
游戏管理器
-
BootUpComponent
-
BootUp Text

图 7.5 – 游戏对象层次结构窗口顺序
在前面的截图右侧,我们选定了 GameManager 游戏对象,显示了其三个主要组件脚本:
-
游戏管理器
-
场景管理器
-
分数管理器
如您所忆,我们的 GameManager 脚本将始终保留在场景中,即使场景被另一个场景替换,因此我们必须在游戏管理器预制件中拥有这些组件。
接下来,我们将把背景从天空更改为灰色,如之前所述。为此,从 层次结构 窗口中选择 主相机。现在,按照以下步骤操作:
-
在 检查器 窗口中,点击 清除标志 选择项,并将其从 天空盒 更改为 纯色。
-
在
32323200下方。 -
这将把 RGB 值更改为
50,50,50,并设置透明度为零。
使用以下截图作为 清除标志、背景 和 十六进制颜色 位置的参考:

图 7.6 – 更改场景的背景颜色
这将把 游戏 窗口中的背景更改为灰色。
接下来,我们将选择 BootUpText 并添加一个位于屏幕底部中央的 文本网格。按照以下步骤操作:
-
在 层次结构 窗口中选中
BootUpText游戏对象。 -
然后,在 检查器 窗口中,点击 添加组件 按钮。
-
在下拉菜单中输入
Text Mesh,直到你在下拉列表中看到它。 -
从下拉菜单中选择 文本网格。
在仍然选中 BootUp Text 游戏对象的情况下,将其 变换 位置更改为以下内容:

现在我们已经将文本放置在正确的位置,我们需要在 检查器 中填写 文本网格 组件。按照以下步骤操作:
-
在
BootUp。 -
将 锚点 设置为 居中。
-
将 对齐 设置为 居中。
-
打开 游戏 窗口(快捷键:Ctrl (command 在 Mac 上) + 2*)。现在,我们应该有一个灰色屏幕和白色文本,这样我们就可以轻松地识别我们所在的场景。以下截图显示了 “BootUp” 文本的设置,以及其 检查器 属性供参考:

图 7.7 – 基本的 'BootUp' 场景
对于这个 bootUp 场景,我们最后需要做的就是让它像大多数 bootUp 屏幕一样工作。
当 bootUp 屏幕出现时,它会停留几秒钟,然后转到下一个场景。
要使 bootUp 屏幕在几秒钟后加载到下一个屏幕,我们需要创建一个脚本并将其添加到 BootUpComponent 游戏对象中。
- 当我们创建脚本时,需要将其存储在我们其他脚本所在的
Assets/Script文件夹中。
如果你忘记了如何创建脚本,请查看第二章中关于通过脚本更新相机属性的部分,添加和操作对象。
- 将脚本命名为
LoadSceneComponent。
以下截图显示了在层次结构窗口中选择BootUpComponent游戏对象时应看起来是什么样子:

图 7.9 – Load Scene Component将加载'title'场景
如此简单 – 我们不需要担心其他任何事情,因为bootUp场景不是游戏循环的一部分。bootUp场景仅在游戏开始时播放一次。
-
将以下代码输入到
LoadSceneComponent:using UnityEngine.SceneManagement;using UnityEngine;public class LoadSceneComponent : MonoBehaviour { float timer = 0; public string loadThisScene; void Start(){ GameManager.Instance.GetComponentInChildren <ScoreManager>().ResetScore();} void Update() { timer += Time.deltaTime; if (timer > 3) { SceneManager.LoadScene(loadThisScene); } }} -
完成后,保存脚本。
-
返回 Unity 编辑器,并在检查器窗口的
loadThisScene变量字段中输入title,如前一个截图所示。 -
保存
bootUp场景并按bootUp,场景应该加载,然后3秒后加载title场景。
我们现在可以重复在bootUp场景中做的多数操作,并将这些操作复制到title和gameOver场景中。我们将在下一步进行操作。
设置标题和游戏结束场景
我们在上一节中设置bootUp场景的方式与我们想要在添加任何新艺术和自定义功能之前title和gameOver场景看起来和表现的方式相似。
幸运的是,使用 Unity,我们不需要从头开始重复创建这两个场景的整个过程。我们可以复制、粘贴并重命名在bootUp场景中已经创建的游戏对象。
要复制灰色背景和白色bootUp场景,请执行以下操作:
-
在 Unity 编辑器中,当
bootUp场景仍然处于活动状态时,从层次结构窗口中选择所有5个游戏对象(点击列表的顶部或底部,按住Shift,然后点击列表的任一端以选择所有对象)。 -
按左 Ctrl (Mac 上的 Command) + C 复制这些
5个游戏对象。 -
从
Assets/Scene打开title场景。 -
在层次结构窗口中选择并删除所有游戏对象。
-
在
bootUp游戏对象的开放空间中点击任何位置。 -
在
TitleText中选择BootUpText。 -
当
TitleText游戏对象仍然被选中时,更改Title。 -
在
TitleComponent中选择BootUpComponent。 -
在检查器窗口中,仍然选择
TitleComponent游戏对象,点击LoadSceneComponent(脚本)旁边的三个小点。 -
将出现一个下拉菜单;从中点击移除组件。
现在,我们需要为TitleComponent游戏对象编写一个脚本,以便当玩家轻触或点击鼠标按钮时,将加载下一个场景shop。
-
重复与
BootUpComponent相同的步骤来创建和附加脚本,但这次,将脚本命名为TitleComponent(同样,与TitleComponent脚本一样,确保将其移动到Assets/Script中的正确文件夹)并粘贴以下代码:using UnityEngine.SceneManagement;using UnityEngine;public class TitleComponent : MonoBehaviour { void Update() { if (Input.GetButtonDown("Fire1")) { SceneManager.LoadScene("shop"); } } void Start() { GameManager.playerLives = 3; }}
与之前的BootUpComponent脚本相比,TitleComponent脚本的区别在于,当在BootUpComponent中按下并释放鼠标按钮(或在触摸屏上用手指触摸)时,TitleComponent将进入下一个场景(shop场景),这依赖于计时器将过去的3秒增加到加载下一个场景,其安全措施是在玩家完成游戏时重置游戏分数。
- 保存
TitleComponent脚本和title场景。
以下截图显示了在 Unity 编辑器中title场景应该看起来是什么样子:

图 7.10 – 'title'场景游戏对象层次结构顺序
现在,我们需要对gameOver场景重复完全相同的步骤。
从Assets/Scene打开gameOver场景并重复粘贴和重命名游戏对象的步骤。执行以下操作:
-
在
BootUpComponent游戏对象到GameOver。 -
仍然在
BootUpText到GameOverText。 -
在
LoadSceneComponent组件中选择GameOver组件,直到我们在列表中看到它。然后,如果我们还没有添加该组件,就选择它。
以下截图显示了具有相同LoadSceneComponent脚本的GameOver组件,我在loadThisScene变量字段中添加了"title":

图 7.11 – 加载场景组件正在加载'title'场景
- 保存
gameOver场景。
我们的 Unity 项目现在已准备好运行其完整游戏循环。我们将在下一节中更详细地讨论游戏循环。
展示游戏循环已完成
在本节的最后,我们将确认在本章中我们取得了什么成果。我们的游戏现在有一个游戏循环,所以如果我们加载启动场景并在 Unity 编辑器中按下播放,序列将如下所示:
-
启动: 场景运行3秒后移动到标题场景。 -
标题: 如果玩家按下鼠标按钮,将加载商店场景。 -
商店: 玩家按下level1。 -
level1: 玩家在30秒后(level3为45秒)完成关卡或死亡。如果玩家死亡超过3次,他们将看到游戏结束场景。 -
level2: 应用与level1相同的规则。 -
level3: 应用与level1相同的规则,但如果玩家完成关卡,他们将看到游戏结束场景。 -
游戏结束: 场景运行3秒后移动到标题场景。
以下图像显示了我们的游戏循环在各个场景之间移动的过程,然后回到标题场景:

图 7.12 – Killer Wave 的游戏循环
小贴士
记住,如果我们的任何场景看起来比平时暗,我们需要手动烘焙其灯光,就像我们在第三章中做的那样,管理脚本和进行模拟测试。
通过这种方式,我们创建了一系列承担各自职责的场景。当一个场景结束时,无论是由于自身选择还是玩家提示,下一个场景将加载。最终,玩家要么完成所有三个级别,要么失去所有生命,我们的游戏将进入游戏结束场景。从游戏结束场景,我们将玩家送回到标题场景。这是我们游戏循环,这也是每个游戏都会有的。游戏循环是游戏开发的基本要求,而且这也可能在考试中提到。
这部分和这一章的内容到此结束,我们创建并管理了场景以创建游戏循环。
摘要
在本章中,我们创建了一个游戏循环;这些是游戏开发的基础,有时也是应用开发的基础。为了为我们的项目创建游戏循环,我们需要多个具有各自目的的场景。我们还需要知道场景何时开始以及何时结束。场景在玩家按下按钮继续时结束,例如7场景,或者当启动标题在经过几秒钟后自动移动到下一个场景。
除了创建我们的游戏循环之外,我们还学习了一些新的矢量数学组件,包括Mathf.round,用于四舍五入Vector3.distance,用于测量两个Vector3点之间的距离;以及Vector3.lerp,用于在两个Vector3点之间进行插值。
这些是游戏开发中有用的组件,也可能会在考试中提到。
在下一章中,我们将通过自定义字体、创建自己的图像和在 Unity 编辑器中应用一些 UI 动画来为我们的占位符场景添加一些润色。
模拟测试
-
从程序员的角度来看,但同时又不会干扰艺术家在同一工作流程中工作的最佳方式是什么?
-
确保每个 UI 组件都有自己的类,这样任何艺术上的更改都不会影响结果。
-
给每个 UI 组件分配一个单独的材料,以便任何代码更改都将被隔离。
-
为每个 UI 组件使用预制件,以便任何艺术家都可以单独修改它们。
-
有一个单独的脚本遍历所有 UI 组件,检查所做的任何更改,以便让每个人都知道。
-
-
一个Image组件在其Source Image参数中有一个精灵,其Image Type设置为Filled。Filled的作用是什么?
-
填充精灵中的开放空间。
-
提供了多种填充精灵的方法。
-
确保没有其他精灵可以覆盖它。
-
反转精灵的颜色。
-
-
CrossPlatformInputManager替换了哪个组件?-
anyKey -
Input -
mousePosition -
acceleration
-
-
当测试你刚刚开发的上向下射击游戏时,你希望控制具有“街机”感。为了使控制能够在移动玩家时自动对齐位置,哪个属性有助于创建所需的效果?
-
GetAxisRaw -
GetJoystickNames -
InputString -
gyro
-
-
编写代码时,例如变量名,应该使用哪种命名规范?
-
帕斯卡命名法
-
小写
-
蛋糕命名法
-
骆驼命名法
-
-
你正在与一个团队一起创建一个用于军事的逼真模拟,该模拟包括一系列爆炸。你被要求接替之前开发者的工作,他到目前为止已经创建了一个从预制件库中发出一系列爆炸的框架。这些预制件由团队中的艺术家定期更新。尽管看起来很令人印象深刻,但程序已经变得相当庞大,艺术家将需要有权更新、替换、替换和从框架中删除预制件。
你能为团队提供什么解决方案,以防止该框架违反 SOLID 原则,并且对团队中的艺术家来说是可访问的?
-
创建一系列预制件,这些预制件包含一个预制件簇,每次在 Unity 场景中使用时都会随机化。
-
创建一个可脚本化的对象,该对象包含一个预制件数组,该数组引用了任意的脚本。
-
创建一个非过程式的粒子系统,使其能够生成自己的爆炸效果。
-
在运行时将场景中的所有爆炸保留在摄像机之外,然后使用随机选择脚本引入所需的爆炸。
-
哪个碰撞器对 Unity 物理系统来说计算速度最快?
-
轴承
-
球体
-
网格
-
矩形
-
-
哪个 MinMaxCurve 是最便宜的?
-
优化曲线
-
在两个常量之间随机选择
-
在两个曲线之间随机选择
-
常量
-
-
哪个属性需要通过代码访问以创建夜总会场景的闪烁效果?
-
color.a -
spotAngle -
range -
intensity
-
第八章:第八章:添加自定义字体和 UI
在本章中,我们将把上一章为我们的游戏循环创建的场景拿来进行各种定制,重点关注文本、图像和动画。
Unity 程序员考试的要求不仅是要对自己的 C#编程技能有信心,还要熟悉 Unity 编辑器在组件和工具方面提供的功能。因此,在本章中,我们将不进行编程,而是专注于我们的用户界面(UI),它由图像和文本组件组成。还值得一提的是,我们将使我们的 UI 根据屏幕的比率大小进行扩展和收缩,这是仅使用 3D 资产无法实现的(请参阅上一章以获取更多详细信息)。在学习我们的文本组件的同时,我们还将导入并应用我们自己的自定义字体。最后,我们将使用动画器对 UI 进行动画处理,并利用动画控制器,这涉及到创建我们自己的状态。
以下截图显示了本章结束时我们的标题屏幕应该看起来像什么:

图 8.1 – Killer Wave 标题屏幕
本章我们将涵盖以下主题:
-
介绍 Canvas 和 UI
-
将文本和图像应用到我们的场景中
到本章结束时,你将更有信心地将文本和图像组件结合起来,并对 UI 进行动画处理。
本章涵盖的核心考试技能
编程核心交互:
- 实现和配置游戏对象的行为和物理。
在艺术管道中工作:
-
理解材质、纹理和着色器,并编写与 Unity 渲染 API 交互的脚本。
-
理解 2D 和 3D 动画,并编写与 Unity 动画 API 交互的脚本。
开发应用程序系统:
- 解释应用界面流程的脚本,例如菜单系统、UI 导航和应用设置。
技术要求
本章的项目内容可在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_08找到。
你可以在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition下载每个章节的项目文件。
本章的所有内容都包含在章节的unitypackage文件中,包括一个包含本章我们将执行的所有工作的Complete文件夹。
查看以下视频以查看代码的实际应用:bit.ly/3kqIi8k。
介绍 Canvas 和 UI
画布的目的通常是持有图像和文本的 2D 世界。其主要目的是允许用户与事物交互,例如点击按钮、推动音量滑块和旋转旋钮,这通常被称为 UI。
Unity(相当令人困惑地)使得 2D 画布也共享与 3D 世界相同的空间。因此,在我们的场景中,我们通常会有一个大的画布区域,包含 UI;然后,在屏幕的左下角,我们将有我们的 3D 世界。
以下屏幕截图显示了具有实现 Canvas 组件的 Unity 场景示例,以及一个立方体和一个 UI 按钮:

图 8.2 – 我们的按钮的可见性不同于 2D 画布空间和 3D 世界空间
如前一个屏幕截图所示,在右侧,我们有 Game 视图显示一个 3D 立方体和 UI 按钮。在左侧,我们有 Scene 视图显示相同的立方体,但缺少 按钮。这是因为,在 Scene 窗口中,包含 UI 按钮的画布位于其自己的 2D 空间中。为了解决这个问题,我们需要从 Scene 视图中缩放出来,我们将看到 UI 按钮的位置。此外,我们还将看到代表屏幕比率的白色大矩形轮廓。
注意,由于我们放大了很多,3D 立方体实际上非常小。在下面的屏幕截图中,我们甚至看不到立方体,左侧用圆形轮廓标记。一开始理解起来有点复杂,但可以将其视为两个项目共享同一空间:

图 8.3 – 左侧的画布包含我们的按钮,右侧显示其在游戏窗口中的位置
通过这个简短的例子,我们可以看到画布如何与 3D 空间共享空间,接下来让我们进入下一节,我们将开始使用画布并添加一些文本和图像。
将文本和图像应用到我们的场景中
在本节中,我们将更改以下场景:
-
灰色的背景
-
白色的 TextMesh(3D 文本,不需要画布)
我们将用以下场景替换那些场景:
-
黑色的背景
-
自定义红色 Text(2D 文本,需要画布)
如介绍中所述,这样做的好处是文本的大小将保持不变,无论屏幕的比率或分辨率如何。
以下屏幕截图显示了左侧的当前 BootUp 场景和右侧在做出更改后应该看起来像什么:

图 8.4 – 我们将改进 BootUp 场景的视觉效果
如果你还没有在 Unity 编辑器中打开场景,请从 Assets/Scene 跳转到 bootUp 场景。
让我们从将背景颜色从灰色更改为黑色开始。如果您忘记了如何操作,请按照以下步骤进行:
-
在 Hierarchy 窗口中选择 Main Camera。
-
使用
0,0,0,255,如下截图所示:

图 8.5 – 将我们的 bootUp 场景背景更改为黑色
将 BootUp 文本替换为我们的新 2D 文本。
- 从 Hierarchy 窗口中选择
BootUpText并删除它。
我们现在将向场景中添加 Canvas 和 2D 文本。
- 在 Hierarchy 窗口的空白区域右键单击,选择 UI | Text,如下截图所示:

图 8.6 – 在 Unity 编辑器中创建 2D 文本
由于我们添加了 2D 文本,Unity 会自动将 Canvas 和 EventSystem 添加到场景中。以下截图显示了 bootUp Text 游戏对象:

图 8.7 – 我们显示 Canvas、Text 和 EventSystem 的层次结构
- 右键单击
Text游戏对象并将其重命名为presented。
在 Hierarchy 窗口中仍然选择 presented,注意 Inspector 窗口,因为我们需要更新其 Text 组件。
- 在 Text 字段中,将默认输入从 New Text 更改为 presented by。
让我们继续修改 presented 游戏对象的 Text 组件设置。
我们将更改字体,从典型的 Assets/Font/ethnocentric rg it。我们可以从我们的 presented 游戏对象中选择此字体。
-
点击下拉列表中
ethnocentric rg it右侧出现的圆形小圈。 -
更改
0。
到目前为止,我们的字体已从 Scene 窗口中消失。这是因为 矩形工具 不够大,我们需要调整其大小。我们将在下一步进行此操作。
信息提示
矩形工具是图像或文本放置的区域。将其视为与变换组件类似的一个工具,我们在此输入游戏对象的 Position、Rotation 和 Scale 的 Vector3 值,这是我们自 第二章,添加和操作对象 以来一直在更改的。
- 在仍然选中的
presented游戏对象中,确保选择 矩形工具,如下截图所示:

图 8.8 – 矩形工具按钮位置
- 确保我们处于 2D 模式(并且 Gizmos 在条形上已开启),因为我们调整 2D 文本时不需要关心 3D 空间。可以在键盘上按 2 或点击以下按钮:
图 8.9 – 2D 按钮位置
- 此外,检查游戏窗口的比例是否设置为1080 (1920x1080)(这是我们第二章,添加和操作对象中设置的屏幕比例)。您可以使用游戏标签下面的下拉列表来完成此操作,如下面的截图所示:
![图 8.10 – 将我们的游戏窗口宽高比更改为 1080]

图 8.10 – 将我们的游戏窗口宽高比更改为 1080
- 点击并拖动矩形工具的外边缘到最左侧,直到它剪切到外边缘,如下面的截图所示:
![图 8.11 – 点击并拖动我们的矩形工具边缘(箭头所在位置)到最左边的外边缘(*符号)]

图 8.11 – 点击并拖动我们的矩形工具边缘(箭头所在位置)到最左边的外边缘(*符号)
-
完成后,将矩形工具的右边缘拖到外边缘的右侧。
-
现在,将矩形工具的顶部和底部边缘加宽。这样,高度大约是白色外矩形四分之一。我们现在应该能看到文本重新出现,我们的矩形工具比例应该与以下截图中的类似:
![图 8.12 – 按照此处类似的方式,点击并拖动矩形工具的顶部和底部边缘]

图 8.12 – 按照此处类似的方式,点击并拖动矩形工具的顶部和底部边缘
现在我们已经设置了矩形工具的间距,我们需要设置锚点,以确保文本无论屏幕比例或分辨率如何都能保持正确的尺寸。
在画布屏幕的中心,我们应该能看到四个指向彼此的箭头(以下截图的左侧用红色圈出了这四个箭头)。
要将锚点设置在四个蓝色圆圈相同的位置,请执行以下步骤:
- 逐个点击并拖动每个白色箭头到每个蓝色圆圈的位置:
![图 8.13 – 点击并拖动每个白色轮廓箭头到其远角的蓝色圆圈]

图 8.13 – 点击并拖动每个白色轮廓箭头到其远角的蓝色圆圈
现在既然我们的锚点大致位于矩形工具上方,该工具由蓝色圆圈表示,我们可以确保位置和锚点都对齐。
- 要将矩形变换设置到位,请在左、顶、Pos Z、右和底位置输入值为 0。以下截图(左侧)显示了我们的突出显示值。您的可能不会相同,因为我们之前手动定位了矩形工具:
![图 8.14 – 将所有突出显示的矩形变换值重置为 0]

图 8.14 – 将所有突出显示的矩形变换值重置为 0
现在,我们可以继续在检查器窗口中的文本组件中设置文本的颜色和位置。
- 要在文本组件中居中由文本,选择对齐部分中的两个中间按钮,如下一个截图所示:

图 8.15 – 更新每个文本组件属性值
-
打开最佳拟合框。这将确保我们的文本可以动态地缩放以支持屏幕的比率。
-
使用
0和80。 -
通过点击颜色字段并选择红色来更改颜色,如前一个截图所示。
我们现在已经完全运行了我们之前拥有的bootUpTextMesh。
以下截图显示了我们的自定义文本、颜色、大小和对齐方式:

图 8.16 – 我们的新字体现在居中并具有风格
如你所想,我们还没有完全完成,因为我们还需要在由文本下面显示名字或公司名称。幸运的是,我们只需要重复我们刚刚完成的工作的四分之一左右。而且,正如你可能猜到的——是的——我们可以复制并粘贴这段文本。
要设置自己的名字或公司名字在由下面,请执行以下步骤:
-
从层次结构窗口中选择
presented游戏对象。 -
按Ctrl(在 Mac 上为command)和D键复制游戏对象。
-
按T键切换到移动工具:

图 8.17 – 复制文本的第二行
-
现在点击并拖动绿色y 轴箭头向下(如前一个截图所示),大致位于原始由文本的矩形工具所在的白色线上。
-
接下来我们需要做的就是点击并拖动锚点的四个白色箭头轮廓向下,以适应我们新创建的游戏对象,如下一个截图所示:

图 8.18 – 整理我们复制的文本的 Rect Transform 边界
- 如果你已经移动了
0。
因此,我们的新文本已经到位。我们现在需要做的就是更改输入的内容。
-
我们知道如何做到这一点:只需滚动到文本组件并输入你的名字、公司名字、宠物的名字或任何名字——对于本教程的目的来说,这都不重要。
-
一旦你输入了你的名字,不要忘记将
presented(1)游戏对象重命名为yourName。 -
点击游戏选项卡窗口查看其外观。这就是我们的样子:

图 8.19 – 我们的启动文本已完成
- 保存场景。
我们已经介绍了Canvas组件的基本知识以及如何应用 2D 文本。接下来,我们将重复类似的程序并使用图像组件。这相当于我们在第五章中用于商店场景按钮的Sprite Renderer,即为我们的游戏创建商店场景。然而,在这里,图像组件用于 2D 空间。
从这一点到本章的结尾,我们将按顺序浏览一系列子章节,以便润色和生动化我们的场景。我们将涵盖以下内容:
-
从我们的标题场景开始,我们将通过创建和应用文本和图像组件来改进其视觉效果。
-
我们将使用自定义字体,因为我们能这样做,这将使我们的游戏看起来比 Unity 附带的标准字体更好。从那里,我们将能够进一步自定义
title场景的外观,使其更适合我们的游戏。 -
然后,我们将把应用到
title场景中的内容复制并粘贴到其他场景中。从那里,我们将稍微更改文本内容和位置。 -
最后,我们将设置我们的动画器和动画控制器状态,并动画化我们的 UI 以介绍每个游戏级别。
让我们继续润色我们的title场景。
改善我们的标题场景
在本节中,我们将重复我们在上一节中学到的程序,但不会深入探讨,因为我们已经知道如何创建 Canvas、添加自定义文本和执行复制。在本节中,我们还将利用 Unity 的图像组件。
以下截图显示了我们将要经历的转换,从左侧的当前标题场景开始,使用我们在上一节中应用的技术,以及添加图像组件,以创建右侧的红色条纹:

图 8.20 – 左侧的旧标题屏幕;我们将用右侧的内容替换它
如前所述,我们不会深入所有细节;然而,如果你在任何时候遇到困难,请参考上一节以指导你了解你应该知道的内容。让我们开始吧:
-
让我们从从
Assets/Scene加载我们的title场景开始。 -
将
255更改为。 -
从层次结构窗口中删除
TitleText游戏对象。 -
在层次结构窗口中仅创建一个
Canvas游戏对象。以下截图作为参考:

图 8.21 – 创建 Canvas
现在,我们将创建一个空的游戏对象。在这个游戏对象内部,我们将存储我们的文本和图像组件:
-
通过在层次结构窗口的底部空白区域右键单击,然后从下拉列表中选择创建空对象来创建一个空的游戏对象。
-
新的空游戏对象将默认命名为
GameObject。 -
右键单击此游戏对象并选择
标题。 -
将
标题游戏对象移动到画布游戏对象中,使其成为子对象。
通常,当创建一个新的游戏对象时,它将自动获得一个变换组件,该组件包含游戏对象在 3D 空间中的位置、旋转和缩放。在本节中,我们的重点是 2D 空间,因此我们需要将此游戏对象从变换组件更改为矩形变换组件。
要将标题游戏对象从变换更改为矩形变换,请按照以下步骤操作:
-
在层次结构窗口中选择
标题游戏对象后,在检查器窗口中单击添加组件按钮。 -
将出现一个下拉列表。在搜索栏中输入
rect transform,直到可以从列表中选择它,如下面的屏幕截图所示:

图 8.22 – 从下拉菜单选择矩形变换
我们现在将设置我们的标题游戏对象的标题游戏对象的锚点。
我已将我的标题游戏对象的矩形变换组件设置为以下设置:

图 8.23 – 更新锚点最小和最大值
如您所见,在前面的屏幕截图中,标题游戏对象位于画布的白色框轮廓中央。此外,请注意,矩形变换组件的左、上、Z 位置、右和下位置都设置为 0 的值。
下一步将在标题游戏对象内添加一个红色透明条带。要添加图像组件,请按照以下说明操作:
-
在层次结构窗口中创建一个新的游戏对象。
-
将游戏对象命名为
mainCol。 -
将
mainCol游戏对象拖放到标题游戏对象上方,使mainCol成为标题的子对象。请参考以下屏幕截图以获取参考:

图 8.24 – 从层次结构窗口查看画布子对象顺序
- 在选择
mainCol游戏对象的情况下,我们希望其标题游戏对象保持原样,因为它是我们的mainCol游戏对象的父对象。以下屏幕截图是关于我们的mainCol矩形变换属性的参考:

图 8.25 – 将 mainCol 锚点设置为全尺寸
- 在选择
mainCol游戏对象的情况下,将图像选择到下拉列表中,直到它出现。当它出现时,选择它。请参考以下屏幕截图:

图 8.26 – 从下拉菜单添加图像组件
我们现在已添加了mainCol图像。
- 接下来,我们将调整
mainCol图像反应的颜色变化(表示为3):

图 8.27 – 更新图像组件的颜色设置为显示的值
更多信息
如果我们在参数中添加了精灵而不是仅仅改变颜色,使用图像组件,我们还将有改变其图像类型的能力。可以使用的一种类型称为填充。这可以给你一种精灵正在填充的印象,这对于加载条或倒计时的时间限制将很有用。
如果你想了解更多关于图像组件及其其他用途的信息,请查看docs.unity3d.com/2017.3/Documentation/Manual/script-Image.html的文档。
接下来,我们将使用包含图像组件的另一个游戏对象添加一个条带到我们刚刚制作好的图像顶部。为此,我们将重复我们之前的方法,但使用更紧、更薄的条带。按照以下步骤操作:
-
在
mainCol仍然被选中时,按键盘上的Ctrl (Mac 上的command*) 和 D 来复制游戏对象。 -
将新游戏对象
trim00重命名。 -
将
trim00游戏对象的trim00游戏对象在顶部:

图 8.28 – trim00 锚点最小和最大值
对于我们的 trim 集,我们不需要改变颜色,因为它从mainCol游戏对象中复制而来。我们现在需要重复这个过程来处理mainCol图像的底部部分。
我们需要完成以下步骤来复制另一个 trim 游戏对象:
-
复制
trim00游戏对象,并将其重命名为trim01。 -
将
trim01游戏对象的矩形变换设置设置为以下截图所示的相同设置:

图 8.29 – trim01 锚点最小和最大值
现在是时候按照以下说明输入我们的主标题文本KILLER WAVE了:
-
在层次结构窗口中创建另一个空的游戏对象。
-
给新的空游戏对象命名为
TitleText。 -
在
TitleText游戏对象内部的Title游戏对象中。这样TitleText就变成了Title的子对象。 -
在
TitleText仍然被选中时,在检查器窗口中点击添加组件,然后从下拉列表中选择矩形变换,就像之前一样。 -
将
TitleText矩形变换设置设置为以下值:

图 8.30 – TitleText 锚点最小和最大值
这将填充我们的TitleText Title)。
我们TitleText游戏对象的最后几步是给它添加一个文本组件,并在检查器窗口中设置其值:
-
在
text中选择TitleText游戏对象(如果它不在那里),然后从下拉列表中选择,就像之前一样。 -
在
killer wave中。 -
点击
bootUp场景右侧的小圆圈(ethnocentric rg it)。 -
设置对齐按钮为中心和中间。
-
打开最佳拟合。这将让 Unity 根据其比例尝试调整文本。
-
设置
140。这将给我们一个相当大的标题。 -
在颜色字段中选择鲜艳的红色。
我们已经设置了标题。在这个场景中,我们最后需要做的是在屏幕底部设置一条消息,提示玩家开始游戏。
与我们在bootUp场景中所做的一样,我们可以复制我们的TitleText游戏对象。然而,这次,我们将把复制的游戏对象移动到其父矩形变换的限制之外。我们将显示的最后文本将是一条提示玩家点击屏幕或射击以开始游戏的短信。
要输入SHOOT TO START文本,请按照以下说明操作:
-
在层次结构窗口中选择
TitleText游戏对象。 -
按Ctrl(在 Mac 上为command)和D来复制它。
-
将复制的游戏对象重命名为
shootToStart。 -
在
shootToStart仍然被选中的情况下,在检查器窗口中更改其SHOOT TO START。 -
设置
50。
如前所述,我们将把文本选择从其当前区域移动到其父对象之外。
- 按Ctrl(在 Mac 上为command)并点击其上的任意一个白色箭头。然后,将矩形工具向下拉,使其完全位于其父矩形变换之外,如以下截图所示:

图 8.31 – 将 shootToStart 锚点移动到高亮位置
将SHOOT TO START文本移至白色箭头所在的位置。以下截图显示了文本的放置以及其矩形变换属性值:

图 8.32 – shootToStart 锚点的最小和最大值
我们不需要更改场景的任何功能,因为我们已经设置了这一点。
- 保存场景。
以下截图显示了我们的title场景现在的样子:

图 8.33 – 杀手浪标题屏幕
到目前为止,我们已经通过复制、修改和将其移动到其父矩形工具游戏对象之外来进一步处理我们的文本。我们还引入了图像,并以类似的方式使用它们来处理 2D 文本。
我们现在将继续并开始下一个场景:gameOver。
复制我们的游戏对象
在本节中,我们将改进gameOver场景,从其灰色背景和块状白色文本,替换为与title场景相同的图像和文本。然而,这次,我们不会重复之前章节中的相同步骤来重现相同的结果。
我们将复制、粘贴并调整游戏对象以节省时间和精力,而不是重复我们在标题场景中已经实现的内容。
作为概述,以下是我们gameOver UI 游戏对象将包含的内容和功能:
-
Canvas:这是所有 UI 游戏对象的父对象。 -
GameOverTitle:这包含与文本和图像组件相关的所有单个游戏对象。 -
mainCol:中心的主要红色条纹(包含图像组件)。 -
trim00:顶部的红线(包含图像组件)。 -
trim01:底部的红线(包含图像组件)。 -
GameOverText:主要的GAME OVER文本(包含文本组件)。
幸运的是,我们实际上不必太担心它们的角色是什么,因为我们已经在上一节中建立了这一点。要复制我们的游戏对象并将它们从title场景移动到gameOver,请执行以下步骤:
-
在我们仍然处于
title场景时,按住键盘上的Ctrl(在 Mac 上为command)并选择主相机和Canvas。 -
我们的两个对象都将被突出显示。右键单击其中的任何一个,然后从下拉列表中选择复制。
-
打开
gameOver场景。 -
在
GameOverText中。我们将用我们复制的游戏对象替换这些内容。 -
在层次结构窗口的空白区域右键单击并选择粘贴。
-
在
Title游戏对象旁边的Canvas游戏对象旁边点击箭头,然后从下拉列表中选择重命名。 -
将游戏对象重命名为
GameOverTitle。 -
点击
GameOverTitle旁边的箭头,将TitleText游戏对象重命名为GameOverText。 -
选择
shootToStart游戏对象,然后在键盘上按Delete键。
为了确认我们到目前为止所做的工作,以下截图显示了gameOver场景:
![图 8.34 – 带有序列游戏对象的层次结构窗口
![img/Figure_8.34_B18381.jpg]
图 8.34 – 带有序列游戏对象的层次结构窗口
-
在
GameOverText仍然被选择的情况下,将其killer wave更改为game over。 -
保存场景。
这是我们gameOver场景应该看起来像的:
![图 8.35 – 游戏结束屏幕
![img/Figure_8.35_B18381.jpg]
图 8.35 – 游戏结束屏幕
在本节中,我们发现我们可以在同一个 Unity 项目中简单地复制和粘贴游戏对象,只要我们在同一个场景中工作。这节省了时间和精力,并使我们的游戏看起来与场景的其他部分保持一致。
在下一节中,我们将学习如何对 UI 游戏对象进行动画处理。
准备对 UI 游戏对象进行动画处理
在本节中,我们将使用我们已经介绍过的多种技术,因此不会深入到相同的程度。一旦我们复制并更改了游戏对象,我们还将添加动画元素,使我们的 2D 视觉效果不那么静态。
我们将使用与gameOver场景类似的方法,通过复制我们之前场景的Canvas及其子游戏对象(不是gameOver场景,因为它包含我们需要的元素。这只需要在进入动画阶段之前进行一些修改)。
要设置level1场景,执行以下步骤:
-
确保我们的
gameOver场景仍在 Unity 编辑器中打开。这是因为我们将要复制一些游戏对象到level1场景中。 -
在
Canvas游戏对象中,从下拉列表中选择复制。 -
从项目窗口打开
level1场景。 -
在我们的
level1场景中的Canvas上右键单击。 -
接下来,我们将重命名两个游戏对象以适应我们的
level1场景。 -
在
GameOverTitle游戏对象中扩展Canvas游戏对象。右键单击它,从下拉列表中选择重命名。 -
将游戏对象重命名为
LevelTitle。 -
在层次结构窗口中展开
LevelTitle游戏对象。 -
在
Level中选择GameOverText。
这就是我们需要在LEVEL 1的Canvas游戏对象上做的所有事情。
在选择Level游戏对象的同时,移除LEVEL 1,如下面的屏幕截图所示:
![Figure 8.36 – Level 1 text typed out]
![img/Figure_8.36_B18381.jpg]
图 8.36 – Level 1 text typed out
我们现在可以开始对 UI 2D 文本及其图像进行动画处理了。我们将要动画化的所有游戏对象都位于LevelTitle游戏对象内。
要设置我们的动画,我们需要执行以下操作:
-
当你仍然在
level1场景中时,在层次结构窗口中,选择Canvas游戏对象下的LevelTitle。 -
在
LevelTitle中,点击底部的添加组件按钮。 -
输入
Animator直到你看到单词Animator出现,然后选择它。
我们的LevelTitle游戏对象现在有一个LevelTitle游戏对象。要这样做,请遵循以下步骤:
-
在
Assets/Animator。 -
在项目窗口的空白区域右键单击,然后从下拉列表中选择创建,接着选择Animator Controller。
-
更改新的
LevelTitle的名称。
我们现在需要将新的LevelTitle Animator Controller附加到我们的Animator组件上。
- 在
LevelTitle游戏对象中,点击旁边的小圆形圆圈(在以下屏幕截图中被标记为远程,用箭头表示)从下拉列表中选择。
以下屏幕截图显示了已选择LevelTitle控制器的LevelTitle游戏对象:
![Figure 8.37 – LevelTitle game object with an Animator component and a LevelTitle Animator Controller]
![img/Figure_8.37_B18381.jpg]
图 8.37 – 带有 Animator 组件和 LevelTitle Animator 控制器的 LevelTitle 游戏对象
接下来,我们需要创建一个动画,以便我们可以将其添加到Animator Controller:
-
在
Assets/Animator位置,在空白区域右键单击。选择创建 | 动画。 -
将
levelTitle_A重命名。
现在让我们打开levelTitle_A片段。
- 在 Unity 编辑器的顶部,点击窗口然后选择动画器。
这将打开动画器窗口。
- 在层次结构窗口中选择
LevelTitle游戏对象。动画器的内容将出现,包括其三个状态(任何状态、进入和退出)。
以下截图显示了动画器窗口及其三个默认状态,以及所选动画控制器的位置引用:

图 8.38 – 包含动画控制器文件位置名称的动画器窗口
在我们将动画片段拖入之前,在片段播放前有一个小的时间延迟对我们是有益的;否则,动画可能会播放得太早。为了解决这个问题,我们可以创建一个有时间限制的空状态。我们可以设置这个levelTitle_A)。
要创建一个空闲状态并将其连接到目标动画片段,请按照以下步骤操作:
- 在动画器窗口的空白部分右键单击,并选择创建状态 | 空。以下截图显示了我们应该期望的下拉列表:

图 8.39 – 创建一个空状态
-
选择新状态,然后在检查器窗口中,将新状态名称更改为空闲。然后,按键盘上的Enter键。
-
现在我们可以将
levelTitle_A从项目窗口拖动到动画器窗口。 -
我们现在需要将我们的
Idle游戏状态与levelTitle_A状态之间的转换连接起来。 -
在空闲状态上右键单击,并从下拉列表中选择创建转换。
-
选择levelTitle_A以在两个状态之间建立连接。
以下截图显示了我们的状态现在应该看起来是什么样子:

图 8.40 – 我们的动画状态从 Entry > Idle > levelTitle_A 转换
动画完成后,可能不需要使用动画器窗口,我们可能需要调整延迟。然而,为此,我们需要使用动画窗口,因此最好将其放在屏幕底部。为此,请执行以下步骤:
-
在项目窗口中,从下拉列表中点击添加标签按钮。
-
点击动画。
以下截图与前面的编号项目一致:

图 8.41 – 显示动画窗口
在本节中,我们引入了 GAME OVER UI 艺术并将其文本从其 Text 组件中替换;我们还带来了其精灵(横幅和修剪)和 Image 组件,以给出我们当前所在级别的表示。然后我们使用 Animator Controller 和其状态准备游戏对象进行动画,随后创建了一个空的 动画 片段。
我们现在可以开始动画化下一节中我们的 LEVEL 1 UI 艺术的进入和退出。
动画我们的 UI 级别标题
我们将要动画化两个游戏对象:level1 场景中的级别标题和主要条形栏。在前一节中,我们在 Unity 编辑器底部设置了 动画 窗口。以下截图显示了我们的当前窗口布局设置,可能有助于参考:

图 8.42 – 推荐的 Unity 编辑器窗口布局
关于动画本身,我们将动画化以下内容:
-
移动到屏幕上的级别文本。
-
主要中心条将变红。
-
将退出屏幕的文本。
以下截图显示了这一点:

图 8.43 – 第 1 级的 UI 动画序列
因此,需要动画化的四个主要元素是包含 Level 游戏对象的 Level 游戏对象,它首先包含 2D mainCol、trim00 和 trim01 游戏对象。
动画 2D 文本组件
在本节中,我们将从左到右动画化文本,它将暂停以便玩家有时间阅读,然后它将移出屏幕:
-
在
Canvas游戏对象中展开其内容,如果尚未展开的话。 -
点击
LevelTitle旁边的箭头执行相同的操作。 -
选择
Level游戏对象。 -
在 动画 窗口中,点击记录按钮,如下截图所示:

图 8.44 – 动画记录按钮
通过以下操作,我们将动画轨迹线刮擦(注意刮擦是动画术语,用于拖动我们的时间轴指示器)到 Level 游戏对象,从 Canvas 的中心到 场景 视图中的左侧。
- 在
-2000和2000属性字段中选择并拖动Level游戏对象,以将我们的 2DLEVEL 1文本从 Canvas 视图中移出,如下截图所示:

图 8.45 – 定位 Level 游戏对象 Rect Transform 属性值
现在,我们的 LEVEL 1 2D 文本已经被推到一边,我们可以将动画线向前刮擦。
- 在时间轴数字内点击并拖动,如下截图所示:

图 8.46 – 调整时间轴的显示
-
在动画窗口中,将白色垂直线从0:00拖动到0:34。
-
在
Level游戏对象的Rect Transform Left和Right属性设置为 0。
字段将变为红色以显示更改已被记录。动画窗口中的动画时间轴将从 2D 文本的运动中获得关键帧。
以下截图显示了时间轴上的更改:

图 8.47 – 带有记录区域的动画时间轴
我们显然希望LEVEL 1文本在再次离开屏幕之前停留在原地几秒钟。为了使文本在移动前暂停在中心,请按照以下步骤操作:
-
在动画窗口中,将白色线条从0:34移动到1:25。
-
点击添加关键帧按钮。
以下截图显示了时间轴在1:25,在点击记录按钮时添加了新的关键帧:

图 8.48 – 添加关键帧按钮
- 对于我们的下一个关键帧点,将白色线条拖动到1:50。
现在我们已经到了想要将 UI 文本从其中心位置移动到摄像机视图中不可见的位置的阶段。
-
选择
Level游戏对象。 -
在
2000 -
-2000
这将使LEVEL 1文本从摄像机视图中移出,如下面的截图所示:

图 8.49 – 级别游戏对象 Rect Transform 记录的属性值
-
将鼠标光标向下移动到时间轴动画窗口。然后,点击并按键盘上的F键。这将显示我们刚刚完成的总动画的所有关键帧。
-
在动画窗口的时间轴上点击记录按钮以停止记录,并前后滚动以查看我们的 2D 文本动画进入、暂停然后移出屏幕。
我们通过使用 Unity 的动画系统,在Canvas中动画化了Level 1文本,开始了我们的尝试。
我们的 UI 文本从最左边开始(在摄像机视图中不可见),动画进入中心,暂停,然后动画移出视图。
现在我们可以继续动画化 UI,并将我们的重点从定位转移到在下一节中更改 UI 的颜色(R、G、B、A)到发光的红色。这将表明我们的动画不仅应用于一个组件,而是通过一系列组件共享。我们将接下来动画化图像组件。
动画图像组件的中心条
动画阶段的第二部分是使级别标题的中心条发光红色然后消失。为此,将使用检查器窗口中的mainCol、trim00和trim01游戏对象的图像组件的颜色设置来操作所有动画。
让我们开始动画化所有三个游戏对象的图像组件:
-
在
mainCol、trim00和trim01中。这些是我们将要动画化的游戏对象。 -
在动画窗口中,将线条条完全移回0:00。
-
在动画窗口中点击记录按钮。
-
在所有三个游戏对象仍然被选中的情况下,点击
255、0、0和0。参见图示:



我们的mainCol、trim00和trim01图像组件的透明度颜色属性(注意,透明度是R、G、B和A中的A)。透明度设置将改变图像的透明度:


图 8.51 – 动画窗口中的图像组件关键帧
从基本术语来说,三个游戏对象在场景窗口的动画开始时是不可见的。接下来,我们需要让图像从透明阶段出来并变红。为此,我们现在需要将时间轴指示器移动到0:55并执行以下步骤:
- 在仍然选择三个游戏对象的情况下,将
255、0、0和120更改为如下截图所示:



我们三个游戏对象现在在场景视图中再次可见。动画的最后一部分是让三个游戏对象再次变为不可见。我们不需要回到颜色值设置中,我们可以简单地复制并粘贴我们在时间轴上0:00处创建的键。要复制我们的关键帧,请执行以下操作:
-
在动画窗口中,仍然选择三个游戏对象和记录按钮,将时间轴指示器移回0:00。
-
在动画窗口中,选择所有三个
Image.Color.a更改,如图示:



-
按Ctrl(在 Mac 上为Command)和C复制关键帧。
-
在动画窗口中点击记录按钮以停止录制动画。
-
滚动到1:50并按键盘上的Ctrl(在 Mac 上为Command)和V粘贴。
-
将鼠标光标移至动画窗口。点击并按键盘上的F键以获得时间轴的全视图。移动光标来回查看级别文本如何动画进入场景以及中心条变红。
当我们的第 1 级场景开始时,我们会看到动画前的标题和红色条,这是我们不想看到的。因此,我们需要将第 1 级文本和红色条设置为与我们的动画第一帧相同的值:
-
选择
mainCol游戏对象并设置0。 -
设置
trim00为0。 -
设置
trim01为0。 -
设置
-2000和2000。
以下截图显示了之前步骤的默认位置和 alpha 设置:
![图 8.54 – 我们关卡标题 UI 的默认位置和颜色]

图 8.54 – 我们关卡标题 UI 的默认位置和颜色
- 保存场景。
在我们继续到最后一个部分之前,让我们简要回顾一下我们已经覆盖的内容。我们取出了mainCol、trim00和trim01游戏对象,并使用动画时间线在一系列关键帧上更改了它们的图像组件的颜色alpha 值。
现在,让我们进入下一节,我们将复制我们的艺术作品、文本,在某些情况下,还将复制动画到其他场景中。从那里,我们将调整组件以适应每个场景。
将艺术作品、文本和动画复制粘贴到其他场景中
最后,我们可以复制level1场景(包括其动画)的所有辛勤工作,并将其粘贴到level2和level3场景中,并修改每个关卡编号。为此,请按照以下步骤操作:
-
在层次结构窗口中,选择Canvas,然后按键盘上的Ctrl (Mac 上的Command*) 和 C。
-
在
Assets/Scene中的level2。 -
在
level1场景的Canvas游戏对象的一个开放空间中点击,并选择所有内容。 -
在
Canvas和LevelTitle中选择Level游戏对象。 -
将
Level 1改为Level 2。 -
保存场景。
-
对
level3场景重复此过程。
![图 8.55 – 复制并编辑关卡标题编号]

图 8.55 – 复制并编辑关卡标题编号
干得好!另一个大章节已经被征服。我们已经开始用我们自己制作的精美艺术作品让我们的游戏发光。让我们回顾一下我们已经取得的成就。
我们将一些普通的灰色场景变得更有吸引力,并与我们的科幻游戏更匹配。这一切都归功于 Unity 编辑器,因为我们能够不使用脚本就实现这一点。我们涵盖的主要组件如下:
-
文本: 我们导入了一个自定义字体,并在组件内部调整了它。
-
图像: 对于任何精灵,我们设置颜色以创建一系列带有透明度的红色条纹。
-
动画控制器: 当图像和文本组件需要动画时的保持状态。
-
动画: 每个组件混合的关键帧都在单个时间线动画中设置。
最后,我们不需要通过再次添加 UI 的文本、图像和动画来重复本章开头创建的 UI 过程。我们主要从场景(杀手波浪标题屏幕)中提取了这些游戏对象和组件,并将游戏对象粘贴到现有场景(关卡标题和游戏结束)中,作为游戏主题的模板。一旦这些组件就位,我们只需修改文本字段(将文本从杀手波浪改为关卡 1 等)。
这使得使用 Unity 变得快速且用户友好。我们不需要每次从头开始为新 UI 场景工作。随着我们的 UI 视觉上得到改善,你可能会希望开始觉得我们的项目正变得更加精致。
摘要
本章是关于对我们的游戏项目进行润色,使其现有内容与现有 UI 保持一致。这也要求你在 Unity 程序员考试中理解我们有哪些工具和组件可以帮助我们根据“在艺术管道中工作”的核心考试技能来创建游戏。
我们还取用了 文本 和 图像 组件,并从多个游戏对象中创建了一个动画。这些动画是通过 动画控制器 状态机调用的。
在你的未来项目中,你将有机会在构建游戏循环的同时保持你的 UI 令人印象深刻。
在下一章中,我们将通过使我们的 商店 场景具有多种屏幕比例来扩展我们当前的 UI 技能。此外,我们还将创建一个位于游戏关卡底部的 UI。
第九章:第九章:创建 2D 商店界面和游戏内 HUD
在本章中,我们将关注我们的商店界面以及我们如何从视觉和功能上改进它。当前的商店运行良好,但我们可以使它支持多种屏幕比例。我们还可以引入 Unity 的 Event 系统和 Button 组件,以及一些其他新功能。
本章我们将访问的另一个区域是游戏内的抬头显示(HUD)。这在游戏中很常见,我们在屏幕的特定位置显示游戏信息。我们将显示玩家的生命值和得分以及迷你地图来显示敌人的位置。这可以在以下截图中看到:

图 9.1 – 我们游戏内的 HUD
本章的另一部分将关于改进我们的商店场景的 2D 视觉,以便在我们可以购买升级方面有选择,并且我们还可以动态地扩展商店的大小。此外,您的商店场景将支持任何横幅比例,与之前不同。以下截图显示了我们的商店在不同比例大小下的样子:

图 9.2 – 左侧显示所有按钮;右侧裁剪了按钮边缘
在前面的截图中,请注意,3:2 的屏幕比例相对于我们的 1920 x 1080 (16:9) 屏幕比例会裁剪掉一些屏幕(您将特别注意到每个屏幕的选择网格间距)。到本章结束时,我们的商店场景将看起来像以下截图所示,无论我们的游戏处于何种横幅比例:

图 9.3 – 我们游戏内的商店具有灵活的 UI 显示(无裁剪)
在本章中,我们将涵盖以下主题:
-
设置我们的 HUD
-
使我们的
shop场景支持替代屏幕比例 -
应用和修改我们的
shop脚本
让我们从回顾本章将涵盖的核心考试技能开始。
本章涵盖的核心考试技能
以下是在本章中将涵盖的核心考试技能:
-
在艺术管道中工作:
- 理解材质、纹理和着色器,并编写与 Unity 渲染 API 交互的脚本。
-
开发应用程序系统:
-
解释应用程序界面流程的脚本,例如菜单系统、UI 导航和应用设置。
-
解释用户控制的定制脚本,如角色创建器、库存、店面和在应用内购买。
-
分析使用 Unity Analytics 和 PlayerPrefs 等技术实现的用户进度功能,如评分、等级和游戏内经济。
-
分析 2D 悬浮层脚本,如 HUD、小地图和广告。
-
识别用于保存和检索应用程序和用户数据的脚本。
-
-
为场景和环境设计编程:
- 确定实现游戏对象实例化、销毁和管理的方案。
-
针对性能和平台进行优化:
-
识别针对特定构建平台和/或硬件配置的要求的优化。
-
确定适用于 XR 平台的常见 UI 功能和优化。
-
-
在专业软件开发团队中工作:
- 识别用于构建模块化、可读性和可重用性的脚本结构技术。
技术要求
本章的项目内容可在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_09找到
您可以下载每个章节的项目文件的全部内容,在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition。
本章的所有内容都包含在本章的unitypackage文件中,包括一个包含本章我们将执行的所有工作的Complete文件夹。
查看以下视频以查看代码执行情况:bit.ly/3LsxDWC。
设置我们的 HUD
在侧滚动射击游戏中,我们通常会有一些形式的记录,比如玩家有多少生命值,他们的分数是多少,时间限制,升级等。我们将应用一个典型的 HUD 来显示一组类似的信息。了解 HUD 是 Unity 程序员考试的要求之一。
到本节结束时,我们将为我们的游戏创建一个包含以下内容的 HUD:
-
生命值
-
小地图
-
分数
在我们添加我们的 HUD 之前,我们需要决定它将位于我们的游戏屏幕的哪个位置。作为一个例子,我们将选择一个游戏,这样我们可以简要研究其 HUD 信息是如何显示的。
我们将研究一个名为Super R-Type的游戏,可以在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/blob/main/Chapter_09/superRtype.jpg找到。在这里,屏幕底部我们可以看到其 HUD 由以下四个部分组成:
-
技能水平
-
生命值
-
力量条
-
分数
在这些细节的后面是一个黑色背景,这样在读取 HUD 时场景就不会干扰。
因此,在本节中,我们将首先声明 HUD 空间并为其设置深色背景。为此,请按照以下说明操作:
-
在 Unity 编辑器中,导航到项目窗口中的
Assets/Scene。 -
打开
level1场景。 -
加载
level1后,转到Canvas游戏对象,并选择UI | Image。 -
在
Canvas游戏对象中会出现一个名为Image的游戏对象。
从上一章,我们应该知道包含Canvas的游戏对象。
- 右键点击
Image游戏对象并选择background。
到目前为止,我们已经创建了一个包含图像组件的游戏对象。
现在,让我们继续并缩放这个游戏对象到正确的位置,使其可以作为 HUD 的背景使用。按照以下步骤操作:
- 在我们的
background游戏对象仍然被选中时,在检查器窗口中更改矩形变换设置如下:
t

图 9.4 – 背景矩形变换属性设置
我们的background游戏对象应该缩放到与屏幕底部白色条相同的比例并居中,如下面的截图所示:

图 9.5 – 显示我们背景游戏对象位置的游戏窗口
现在,让我们将这个background游戏对象变暗,使其与我们的游戏融合。
- 在我们的
background游戏对象仍然被选中时,在12,13,13,210,如下面的截图所示:

图 9.6 – 背景颜色和 alpha 值
background游戏对象的颜色已从默认的白色变为深色,略带透明。
HUD 的区域已经设置。以下小节将逐一介绍我们的 HUD 的每个部分,并解释如何创建以下内容:
-
GameManager脚本。每个生命值将被整齐地分组。 -
显示分数:脚本已经跟踪了玩家的分数,所以我们只需要使用一个文本组件来保持信息更新。
-
迷你地图:迷你地图将视觉上类似于雷达,玩家将能够看到接近他们的敌人对手的波浪。这个迷你地图将使用一个更宽角度的第二个摄像头,并且只显示彩色点而不是实际的船只本身。
现在,我们可以开始用我们在脚本中已经制作的数据填充 HUD,从玩家的生命值开始。
显示玩家的生命值
玩家开始游戏时有三个生命值。向玩家显示生命值有两种典型方式:显示数字计数或显示每个生命值的小图标。让我们选择后者,因为我们可以使用一些我们之前没有使用过的 Unity 组件。
本节还将包括一些额外的代码,这些代码将被放入我们的GameManager脚本中。此代码将运行检查以查看玩家有多少生命值。对于找到的每个生命值,将创建一个包含图像的游戏对象。
向我们的游戏对象添加水平布局组组件
将创建的所有游戏对象生命值将存储在一个名为lives的游戏对象中。让我们继续工作在 HUD 上并添加lives游戏对象:
在本节中,我们将创建一个游戏对象,该对象将包含一个图像组件,它将成为玩家飞船的象征。我们还将专门调整其大小,使其与其他生命值保持一致。
图 9.7 – lives Rect Transform 属性设置
创建表示生命值的图像
-
在选择
lives游戏对象的情况下,点击检查器窗口中的添加组件按钮。 -
在
Canvas游戏对象中,选择UI然后从下拉列表中选择图像。
图 9.8显示了lives游戏对象。我们需要调整每个life图像的一些值,以便它们不要太大了。
- 将水平布局组属性值更改为以下截图所示(您可能需要单击填充旁边的箭头以展开其内容):
创建表示生命值的图像
选择游戏对象,右键单击它,然后从下拉列表中选择重命名。
图 9.8 – Horizontal Layout Group 属性值
到目前为止,我们已经创建了一个名为lives的游戏对象,用于存储和自动排序每个玩家的飞船图像。
在下一节中,我们将创建一个游戏对象,用于存放每个玩家的飞船图像。作为下一两节将要介绍内容的示例,以下截图展示了我们的lives游戏对象如何存放每个life游戏对象:


Figure 9.11 – Our ship sprite life
我将稍微改变图标的颜色,因为它可能对玩家来说有点分散注意力。
-
在选择
life游戏对象的情况下,点击153、177、177、255。 -
确保在图像组件中勾选保留纵横比框,以便我们的生命值不会失去其比例。
这就是我们的life游戏对象创建完成。我们需要对它做的最后一件事是将它转换成一个预制体。作为提醒,预制体的好处是我们可以创建一个包含其组件、首选项和设置的完整游戏对象。预制体将允许我们创建所需数量的克隆。
要将此life游戏对象转换为预制体,请执行以下操作:
-
在
Assets/Resources文件夹中。 -
从
Prefab文件夹中拖动life游戏对象到场景中。这就是我们创建的预制体。
我们现在可以在层次结构窗口中删除life游戏对象,因为我们将在下一节中用代码创建这个游戏对象。
编写我们的 UI 生命计数器代码
在本节中,我们将重新访问GameManager脚本,获取玩家的生命计数信息,并以我们的 UI 系统形式显示。
以下截图显示了level1场景的Canvas游戏对象的一部分。在Canvas中,顶部是 HUD 的background游戏对象,然后是lives游戏对象。最后,通过我们的代码(我们将在下面编写),我们在lives游戏对象内部创建了三个life游戏对象:


Figure 9.12 – Our Hierarchy containing a lives game object and its life children
要实例化life游戏对象,使它们显示与玩家生命值相同数量的对象,请执行以下操作:
-
在
Assets/Script文件夹中。 -
双击文件以打开
GameManager。
GameManager脚本已经有一个Awake()函数,这是脚本一旦激活就会尝试激活的第一个函数。我们目前没有的是在Awake()之后被调用的Start()函数。
我们可以在GameManager中创建一个Start()函数,并让它调用我们即将创建的方法,即SetLivesDisplay,并将我们的playerLives变量发送给它,这是玩家生命值的计数。
就像任何函数一样,我们可以在类内的任何位置放置它,只要它不在另一个方法/函数内部。我通常将 Awake() 和 Start() 方法放在 GameManager 类的顶部附近。要在 Start() 方法中调用自定义方法,请执行以下操作。
-
在
GameManager脚本中输入以下代码:void Start() { SetLivesDisplay(playerLives); }
现在,我们将填充 SetLivesDisplay 方法的具体内容。
我已经将 SetLivesDisplay 方法放置在 GameManager 脚本的底部附近,但就像 Start 和 Awake 方法一样,您可以在 GameManager 脚本中的任何位置放置它们。
-
输入以下代码:
public void SetLivesDisplay(int players) {
此方法设置为 public,因为我们的 ScenesManager 脚本将需要访问它来加载玩家所在的任何关卡。我们将 SetLivesDisplay 方法设置为 void,因为我们在这个方法中不返回任何内容。如我之前所述,我们接受 playerLives 整数,但在方法中我们将将其称为 players。
让我们继续,在 SetLivesDisplay 方法内部添加一些代码。这里我们将检查、添加,并在玩家死亡时视觉上移除生命值。
-
在
SetLivesDisplay方法内部输入以下代码:if (GameObject.Find("lives")) { GameObject lives = GameObject.Find("lives"); if (lives.transform.childCount < 1) { for (int i = 0; i < 5; i++) { GameObject life = GameObject.Instantiate(Resources .Load ("life")) as GameObject; life.transform.SetParent(lives.transform); } }
在前面的代码中,我们运行了一个检查以找到名为 lives 的游戏对象。如果我们找到游戏对象,我们将将其引用存储在名为 lives 的游戏对象中。然后我们运行了一个检查以查看我们的 lives 游戏对象是否持有任何游戏对象。如果 lives 没有持有任何游戏对象,我们假设这是关卡开始,并且我们需要创建一些生命值。在 if 语句内部,我们运行了一个限制为 5 次数的 for 循环。在这个 for 循环内部,我们实例化我们的 life 预制件,并让它位于 lives 游戏对象内部。
-
继续在
SetLivesDisplay方法内部编写,这是管理每个life预制件计数的部分,并显示玩家实际拥有的生命值数量://set visual lives for (int i = 0; i < lives.transform.childCount; i++) { lives.transform.GetChild(i).localScale = new Vector3(1,1,1); } //remove visual lives for (int i = 0; i < (lives.transform.childCount - players); i++) { lives.transform.GetChild(lives.transform.childCount - i -1).localScale = Vector3.zero; } } }
我们刚刚编写的代码有两个主要部分。第一个 for 循环是根据 lives 游戏对象下的游戏对象数量设置的。每个在 lives 下的游戏对象都会缩放到 1。
第二个 for 循环从 lives 下的游戏对象计数中减去,与传入此方法参数的玩家的 int 变量相减。在这个第二个 for 循环中——根据玩家 int 变量的大小——每个 life 预制件都会缩小到 0。将 life 预制件缩放到 0 不会影响生命值计数,使其不会根据显示的生命值数量波动。
- 保存脚本。
GameManager 现在能够在 level1 场景的底部创建生命计。我们现在需要添加一些功能,以便 ScenesManager 在加载关卡时加载生命值数量。
要在关卡开始或玩家死亡时加载玩家的生命值,请执行以下操作:
-
在
ScenesManager脚本中,Assets/Script。 -
双击
ScenesManager脚本以开始编码。 -
在
ScenesManager脚本中,我们将添加一个Start()函数,该函数将包含一个已知的 Unity 委托sceneLoaded,它由 Unity 的SceneManager调用。此委托将订阅我们的游戏场景变化时的情况。有关sceneLoaded委托的更多信息,请访问docs.unity3d.com/ScriptReference/SceneManagement.SceneManager-sceneLoaded.html。 -
在
ScenesManager脚本中,输入Start函数,以及我们要挂钩到委托的函数名称:void Start() { SceneManager.sceneLoaded += OnSceneLoaded; }
仍然在ScenesManager脚本中,我们将添加 Unity 识别的函数,即使我们不会对Scene和LoadSceneMode类型做任何事情,它也会自动获取这些类型。
在函数内部,我们调用GameManager脚本的SetLivesDisplay,以及玩家拥有的生命值数量。
-
在
ScenesManager中输入我们刚才讨论的以下代码:private void OnSceneLoaded(Scene aScene, LoadSceneMode aMode) { GetComponent<GameManager>().SetLivesDisplay(GameManager.playerLives); } -
保存脚本。
让我们检查我们做了什么:
-
在仍然处于我们正在工作的场景(
level1)的同时,返回 Unity 编辑器。 -
按Play – 应该显示三个生命值。如果玩家死亡,生命值计数将降至两个。
以下截图显示了正在进行的游戏,以及玩家生命值位于左下角:
![Figure 9.13 – Screenshot of our game currently with its lives counter]
![img/Figure_9.13_B18381.jpg]
Figure 9.13 – Screenshot of our game currently with its lives counter
在本节中,我们已经将玩家的生命值连接起来,以便它们可以在 HUD 的左下角显示。我们已应用了如Horizontal Layout Group和Layout Element等组件,以设置玩家生命图像的均匀顺序和大小。我们还确保代码在场景加载时应用并更新玩家的生命值。
接下来,我们将关注 HUD 的另一侧并显示玩家的分数。
显示玩家的分数
在本节中,我们将把玩家的分数应用到 HUD 的右侧,我们目前正在用有关玩家的信息填充 HUD。
我们将继续在Canvas游戏对象中工作,并添加另一个名为score的游戏对象。在这里,我们将添加ScenesManager代码以加载分数显示。让我们开始吧:
-
在仍然处于
level1场景的情况下,在Hierarchy窗口中右击Canvas游戏对象。 -
从下拉列表中选择UI | Text。
-
右击新的
Text游戏对象,从下拉列表中选择Rename。 -
重命名游戏对象
score。
将score游戏对象重命名并放置在Canvas游戏对象内部后,接下来我们需要做的是调整score游戏对象的大小并将其移动到合适的位置。
- 在仍然选择
score游戏对象的情况下,在Inspector窗口中修改其Rect Transform属性,使其看起来像以下截图所示:
![Figure 9.14 – score Rect Transform property settings]
图 9.14 – 分数矩形变换属性设置
当score游戏对象处于正确的位置和缩放时,我们现在可以自定义其文本组件设置。
当score游戏对象仍然被选中时,在检查器窗口中对其文本组件进行以下更改:
-
更改
00000000。文本字段中的零的数量将帮助我们指定板的大小。 -
如以下截图所示,我们选择了与游戏关卡场景标题相同的自定义文本。点击字体字段右侧的远程按钮,从下拉列表中选择ethnocentric rg it。
-
将对齐按钮设置为右对齐和居中。这将定位文本并最小化其右侧的空间。
-
score文本字体大小将自动设置。 -
将
0和60更改。这将设置最佳拟合文本的界限。
最后要更改的属性是文本的颜色属性。我们将将其设置为与玩家的生命值相同的颜色。
- 点击
153、177、178、255。
以下截图显示了我们的文本组件属性已被设置为何:

图 9.15 – 分数文本组件属性设置
如果我们检查游戏窗口,我们应该看到右上角的分数大小合适,如下面的截图所示:

图 9.16 – 我们的游戏内 HUD 现在显示分数
我们score游戏对象的最终阶段是更新我们的ScenesManager脚本,通过添加一个if语句来检查score游戏对象是否在场景中。
要更新ScenesManager脚本以支持我们新的分数游戏对象,请执行以下操作:
-
在
Assets文件夹中。 -
双击
ScenesManager脚本并向下滚动到我们输入了OnSceneLoaded函数的位置。 -
在
OnSceneLoaded函数内部,输入以下代码:if (GameObject.Find("score")) { GameObject.Find("score").GetComponent<Text>().text = GetComponent<ScoreManager>().PlayersScore.ToString(); }
如新添加的代码简要提到的,我们正在检查score游戏对象是否在场景中。如果score存在于场景中,那么我们获取其Text组件,并从ScoreManager脚本中将玩家的分数整数应用到它上。同时确保你在脚本顶部有using UnityEngine.UI以访问Text组件。
- 保存脚本。
说到ScoreManager脚本,我们需要重新加载此脚本,以便其ResetMethod可以在每场比赛的开始/结束时重置score UI。按照以下步骤操作:
- 在
Assets/Script中找到ScoreManager脚本并打开文件。
在脚本内部,我们需要引入UnityEngine.UI库,以便我们可以更改游戏的可视分数。
-
在
ScoreManager脚本的最顶部输入以下代码:using UnityEngine.UI; -
在
ResetScore方法中,添加一个if语句,检查scoreUI 游戏对象是否在场景中并更新。以下代码显示了ScoreManager脚本中的完整ResetScore方法:public void ResetScore() { playerScore = 00000000; UpdateScore(); } -
我们还需要将
UpdateScore方法应用到我们的SetScore函数上。按照以下代码进行应用:public void SetScore(int incomingScore) { playerScore += incomingScore; UpdateScore(); } -
我们现在需要将
ScoreManager脚本中的UpdateScore方法移动到UpdateScore方法内。为此,添加以下新方法以在得分设置或重置时更新我们的得分:void UpdateScore() { if (GameObject.Find("score")) { GameObject.Find("score").GetComponent<Text>().text = playerScore.ToString(); } } -
保存脚本。
-
返回 Unity 编辑器并点击
level1。
当我们摧毁敌人时,我们的score游戏对象将更新,如下面的截图所示:

图 9.17 – 游戏得分总计的屏幕截图
在本节中,我们将原本显示玩家得分的现有ScoreManager代码改造成一个score变量,用于level1场景中的新 HUD 得分,当敌人被摧毁时,得分将更新。
我们需要为 HUD 创建的最后一件东西是迷你地图,这将给我们一个在我们关卡中敌人的视觉。
创建迷你地图
在本节中,我们将在 HUD 显示中嵌入一个迷你地图,以显示更大范围的关卡。这将以雷达风格显示玩家以及附近的敌人。以下截图显示了 HUD 中间的雷达,代表玩家及其周围的敌人以及其他即将进入玩家屏幕的敌人:

图 9.18 – 显示迷你地图的游戏截图
我们将迷你地图分为三个部分:
-
雷达相机:场景中的第二个相机。
-
层:这使得第二个相机只能识别特定的一组游戏对象。
-
渲染纹理:在 HUD 上以动画图像的形式显示最终结果。
让我们先创建一个额外的层,以便我们可以将某些游戏对象暴露给我们的雷达相机。
创建并添加层到我们的玩家和敌人游戏对象
在本节中,我们将向玩家和敌人游戏对象添加一个额外的游戏对象,以便我们的第二个相机只能看到附加的精灵。这些将看起来像雷达上的亮点。
要将雷达亮点添加到游戏对象中,请执行以下操作:
- 在 Unity 编辑器的右上角,点击层按钮,然后点击编辑层...,如下面的截图所示:

图 9.19 – 编辑层…将允许我们为游戏添加另一个层
-
检查器窗口将改变并显示标签和层属性。从这里,我们可以点击展开层选项卡。
-
点击顶部附近的一个可用图层,输入
Radar,如下截图所示:![Figure 9.20 – 扩展我们的图层列表并添加一个新图层![Figure 9.20_B18381.jpg]
Figure 9.20 – 扩展我们的图层列表并添加一个新图层
现在,我们可以向玩家和敌人添加雷达点。让我们先从将玩家带入场景并更新其预制开始,使其能被雷达摄像机识别。为此,请按照以下说明操作:
-
在
Assets/Prefab/Player文件夹中。 -
将
player_ship拖放到层次结构窗口底部的空白区域。 -
在
player_ship中,选择创建空对象。 -
右键点击游戏对象,从下拉菜单中选择重命名。
-
将游戏对象重命名为
radarPoint。 -
在选择
radarPoint的情况下,点击Sprite Renderer直到它在组件下拉菜单中显示。接下来,点击其位于检查器窗口顶部的图层,并从下拉列表中选择雷达,如下截图所示。我们还可以将我们的变换属性设置为与以下图像中的相同:
![Figure 9.21 – radarPoint 游戏对象及其雷达图层
![Figure 9.21_B18381.jpg]
Figure 9.21 – radarPoint 游戏对象及其雷达图层
设置了变换属性后,我们现在可以将雷达点精灵拖入精灵字段并更改其颜色:
-
点击Sprite Renderer组件中精灵字段右侧的远程按钮。
-
在下拉列表中开始输入
knob,直到你可以看到它并选择它,如下截图所示:
![Figure 9.22 – 访问和选择默认旋钮精灵
![Figure 9.22_B18381.jpg]
Figure 9.22 – 访问和选择默认旋钮精灵
-
我们现在可以通过点击
0,245,255,255来更改精灵的颜色。 -
在
player_ship游戏对象的预制设置中选择player_ship。 -
在层次结构窗口中选择
player_ship游戏对象,并在键盘上按Delete键。
我们现在已设置玩家飞船,使其准备好被雷达摄像机检测。
下一步是重复相同的步骤为敌人操作,敌人位于Assets/Prefab/Enemies。
- 不经过相同的说明,以下截图显示了我们的敌人
radarPoint游戏对象具有明亮的红色颜色值(255,0,0,0)。如果你遇到困难,只需遵循与玩家飞船的radarPoint相同的步骤:
![Figure 9.23 – radarPoint 属性设置
![Figure 9.23_B18381.jpg]
Figure 9.23 – radarPoint 属性设置
提示
在检查器窗口中,在你完成对预制更改后,点击覆盖 | 应用全部。
一旦我们完成更改并将它们应用到预制中,我们就不再需要enemy_wave游戏对象,因为我们已经将其详细信息保存在预制中。
- 从层次结构窗口中删除
enemy_wave。
我们已经有效地创建了一个追踪器(radarPoint),并将其附加到我们的关卡中的玩家和敌人上。
下一步是添加一个渲染****纹理,它将与场景中的第二个摄像头一起工作。第二个摄像头的信号将被输入到一个渲染纹理中。然后,这个渲染纹理将被放置在屏幕的底部中间,并显示玩家和敌人的位置。
添加和自定义我们的渲染纹理
渲染纹理通常用于在播放模式(运行时)中保持移动图像。我们将使用这个渲染纹理来存储第二个摄像头的信号。这将像我们 HUD 中央的小电视屏幕一样工作。
要创建和自定义渲染纹理,我们将执行以下操作:
-
在
Texture文件夹中,即Assets/Texture。 -
在一个空白区域右键点击,从下拉列表中选择创建,然后选择渲染纹理,如图所示:
![Figure 9.24 – Creating a Render Texture
![img/Figure_9.24_B18381.jpg]
图 9.24 – 创建渲染纹理
提示
如果你没有空白区域可以右键点击,如步骤 2建议的,你可以通过调整图标大小来获得空间。
使用项目窗口右下角的滑块调整你的图标大小。
-
慢慢点击文件名两次,将其重命名为
雷达。 -
在选择
雷达渲染纹理后,我们需要将其大小调整为适合 HUD 的大小,并理想情况下使其更清晰。 -
在
256,256到236,46。 -
将滤波模式从双线性更改为点。
设置渲染纹理的最后一步是将它放置到 HUD 中。按照以下步骤操作:
-
仍然在
level1场景中,在层次结构窗口中右键点击Canvas游戏对象,并选择创建空对象。 -
在层次结构窗口中选择新的空游戏对象,右键点击它,并从下拉列表中选择重命名。
-
将游戏对象重命名为
雷达。
雷达游戏对象将作为任何与游戏对象相关内容的容器。
- 此游戏对象现在需要在 HUD 中定位和调整大小。为此,在检查器窗口中更改
雷达游戏对象的矩形变换属性,如图所示:
![Figure 9.25 – radar Rect Transform property settings
![img/Figure_9.25_B18381.jpg]
图 9.25 – radar Rect Transform property settings
移动和调整雷达游戏对象的大小将为我们提供一个信封窗口,以便渲染纹理可以放置其中,如图所示:
![Figure 9.26 – Our radar game object's placement in the Game window
![img/Figure_9.26_B18381.jpg]
图 9.26 – 我们雷达游戏对象在游戏窗口中的位置
我们现在可以添加另一个游戏对象,它将成为我们刚才创建的雷达游戏对象的子对象。此游戏对象将存储渲染纹理:
-
在层次结构窗口中右键点击
雷达游戏对象。从下拉列表中选择UI,然后选择原始图像。 -
右键点击名为
radarImage的新游戏对象。 -
在仍然选择
radarImage游戏对象的情况下,将其 矩形变换 设置更改为以下截图所示的 检查器 窗口中的设置:

图 9.27 – radarImage 矩形变换属性设置
接下来,我们需要将 雷达渲染纹理 应用到 Raw Image 的 纹理 字段:
-
在仍然选择
radarImage游戏对象的情况下,在 层次 窗口中,点击 Raw Image 组件中 纹理 字段旁边的 远程 按钮。 -
在新窗口顶部的搜索栏中开始输入
radar,直到出现 radar 渲染纹理 并选择它。
这样,我们的 渲染纹理 就制作并设置了。现在,我们可以将其传递到第二个相机。但在我们这样做之前,我们需要添加相机!
添加和自定义第二个相机
在本节中,我们将添加第二个相机,这样我们就只能看到 radarPoint 游戏对象。
让我们从在 level1 场景中设置第二个相机开始:
-
在 层次 窗口中,在空白区域右键点击,从下拉菜单中选择 相机。
-
右键点击新创建的
RadarCam。 -
在仍然选择
RadarCam的情况下,在 检查器 窗口中将其 变换 设置更改为以下截图所示:

图 9.28 – radarCam 变换属性设置
仍然在 RadarCam 选择的情况下,我们需要将其 相机 组件设置更改为以下:
-
清除标志:纯色。对于第二个相机,我们不需要背景中的任何东西,因此像纯色这样的基本设置就足够了。
-
255,0,0,50。这将给我们的雷达一个红色调。 -
剔除遮罩:点击标有 一切 的参数字段。执行以下操作:
-
从下拉列表中选择 无 以删除所有层。
-
再次选择该字段,并选择 雷达(如图 9.27 所示的截图)。通过这样做,我们的相机将只能看到与该层相关的游戏对象:
-

图 9.29 – 从剔除遮罩中选择雷达
-
投影:正交投影。雷达相机是 2D 的,因此不需要透视视图。
-
150。我们的相机视图大小将大于玩家所在的主视图。 -
将
RadarCam添加到雷达的 渲染纹理。
-
我们的
RadarCam不需要看到 雷达 层。从 层次 窗口中选择 主相机,并从其 剔除遮罩 中取消选择 雷达 层。 -
此外,在仍然选择
RadarCam的情况下,点击其 音频监听器 组件旁边的三个点,并删除它。我们已经在场景中有一个监听音频的相机了。 -
最后,我们需要确保
RadarCam是RadarCam在 层次 窗口中的子项。 -
在检查器窗口中点击覆盖 | 应用全部以更新主摄像机预设并保存场景。
现在,如果我们点击 Unity 编辑器中的播放,我们将看到 HUD 中的雷达,其红色色调显示红色点表示敌人,霓虹蓝色表示玩家,如下面的截图所示:
![Figure 9.30 – Our mini-map detecting the player and its enemies
![img/Figure_9.30_B18381.jpg]
图 9.30 – 我们的小地图检测玩家及其敌人
这个小地图没有使用任何代码创建,并使用了两个新组件:渲染纹理,它将保存第二摄像机的输入,以及一个原始图像组件,它将显示最终输出。
在本节中,我们创建了一个功能齐全的 HUD,它包含三个主要部分:玩家的生命值、小地图和玩家的得分。我们使用了 Unity 提供的两个基本 UI 工具来创建 UI 显示。然而,我们还引入了三个新组件,如下所示:
-
水平分组布局:平均分配玩家的生命值
-
渲染纹理:传输第二摄像机的输入
-
原始图像:显示渲染纹理的输入
以下截图显示了最终的 HUD:
![Figure 9.31 – Our HUD is complete
![img/Figure_9.31_B18381.jpg]
图 9.31 – 我们的 HUD 已完整
由于我们已更新了level1场景,我们需要更新level2和level3。最快的方法是删除level2和level3并复制level1,就像我们之前做的那样,这将使我们只需更新Text组件中的级别数字。我们在上一章的结尾做了这件事,所以如果你需要一些指导,请查看。
现在,我们将继续改进现有的shop场景,通过移除 UI 组件的预制多边形。这将使我们接触到使用 UI 事件触发器,并使我们的代码更小、更高效。
使我们的商店场景支持替代屏幕比率
在本节中,我们将对我们的当前shop场景进行修改,使其兼容各种屏幕比率。目前,我们的商店视觉效果由多边形组成,看起来不错,但例如,屏幕底部的按钮选择网格有被边缘裁剪的风险。我们还可以通过使用 Unity 的按钮组件来改变选择按钮的方式,该组件在画布中工作。
![Figure 9.32 – There is a wide range of ratios in what a game is displayed in
![img/Figure_9.32_B18381.jpg]
图 9.32 – 游戏显示的比率范围很广
由于这些 UI 更改,这将减少我们的代码并使其更高效,因为我们将会依赖点击事件。我们将在本节后面讨论这些内容。
让我们从替换shop场景底部的选择网格开始。
升级我们的商店选择
在本节中,我们将移除所有的商店按钮,并用一个具有自己射线投射系统的button替换它们。这个射线投射系统将为我们添加和自定义按钮提供更简单的方法,尤其是在添加或从选择网格中提取按钮时。
在下一节中,我们将通过移除我们的 3D 资产来支持这一变化,以便我们可以用 Unity 自带的 2D 按钮替换它们。
准备我们的商店场景进入 2D 模式
让我们先移除底部的旧选择网格和我们的BUY ?按钮,因为它们与我们的shop场景中的样式相同:
-
如果您还没有加载
shop场景,请定位到Assets/Scene。 -
双击
shop场景。 -
在层次结构窗口中,按住键盘上的Ctrl(在 Mac 上为command)并选择以下截图所示的所示所有游戏对象:

图 9.33 – 从商店场景层次结构中选择这些游戏对象
如果出现打开预制件的窗口,请按键盘上的Delete键。打开它并重复此过程。一旦删除,请按Canvas左上角的返回按钮。
要创建具有自己背景的Canvas,请执行以下操作:
-
在层次结构窗口的下半部分,右键单击,然后从下拉列表中选择UI,接着选择Canvas。
-
在层次结构窗口中右键单击
Canvas游戏对象,然后从下拉列表中选择UI,接着选择图像。 -
右键单击名为
Image的新游戏对象,并从下拉列表中选择重命名。 -
将
Image重命名为backGround。 -
在仍然选择
backGround游戏对象的情况下,将其矩形变换属性更改为以下截图所示:

图 9.34 – backGround矩形变换属性设置
- 我们现在可以给
backGround添加一些颜色。在仍然选择backGround游戏对象的情况下,点击255,0,0,63。
以下截图显示了带有红色色调的backGround游戏对象的位置和缩放:

图 9.35 – 游戏窗口中的backGround
我们现在可以继续到下一节,我们将添加三个游戏对象,它们将控制按钮游戏对象的位置和缩放。
添加布局组组件
在本节中,我们将添加支持我们添加到网格中的按钮间隔的游戏对象。这样做的好处是我们可以控制每个按钮部分的属性,如下面的图所示:

图 9.36 – 按照计划设计的我们的商店场景按钮布局
接下来,我们将创建一个空的游戏对象,并向其添加一个水平布局组,这将保持我们的顶部按钮行顺序:
-
右键点击
Canvas游戏对象,从下拉列表中选择Create Empty。 -
将新游戏对象重命名为
gridTop。 -
在
gridTop仍然被选中的情况下,将其Rect Transform属性更改为以下截图所示的设置:
![Figure 9.37 – gridTop Rect Transform 属性设置
![img/Figure_9.37_B18381.jpg]
Figure 9.37 – gridTop Rect Transform 属性设置
现在由于我们的gridTop已经正确定位,我们可以向其添加一个Horizontal Layout Group:
-
在
gridTop游戏对象仍然被选中的情况下,点击下拉列表顶部的搜索栏中的Horizontal Layout Group,直到你看到Horizontal Layout Group。当这个组出现在列表中时,选择它。 -
给Horizontal Layout Group以下设置:
![Figure 9.38 – Horizontal Layout Group 属性值
![img/Figure_9.38_B18381.jpg]
图 9.38 – 水平布局组属性值
gridTop现在将自动排列升级按钮的顶部行。
我们现在需要重复这个过程,但不需要重复整个程序。按照gridTop的相同步骤进行,但进行以下更改:
-
将下一个游戏对象命名为
gridBottom。 -
给游戏对象设置以下Rect Transform属性:
![Figure 9.39 – gridBottom Rect Transform 属性设置
![img/Figure_9.39_B18381.jpg]
Figure 9.39 – gridBottom Rect Transform 属性设置
-
然后,像之前一样,我们需要添加一个
gridTop。 -
我们然后重复此过程,但这次,对于我们的"AD"和"START"按钮,我们将添加一个Vertical Layout Group组件。
-
如前所述,创建一个空的游戏对象并将其存储在
Canvas游戏对象中。 -
命名一个新的游戏对象为
gridOther。 -
给
gridOther的Rect Transform以下设置:
![Figure 9.40 – gridOther Rect Transform 属性设置
![img/Figure_9.40_B18381.jpg]
Figure 9.40 – gridOther Rect Transform 属性设置
- 如前所述,我们将添加一个
gridOther游戏对象并给它以下设置:
![Figure 9.41 – 垂直布局组属性值
![img/Figure_9.41_B18381.jpg]
Figure 9.41 – 垂直布局组属性值
我们新的、重新设计的选择网格现在支持创建多个自缩放按钮。在下一节中,我们将演示如何创建多个按钮,这些按钮会自动缩放以适应选择网格。
添加 UI 按钮
在本节中,我们将创建一个不需要进行任何尺寸更改的按钮,因为我们之前放置的布局组将处理这个问题。
要在Hierarchy窗口中创建一个gridTop游戏对象并执行以下操作:
-
从下拉列表中选择UI然后Button。
-
右键点击新创建的
Button游戏对象,并将其命名为00。
我们将得到一个将被拉伸并放置不正确的按钮,但不用担心 – 这是正常的。稍后,当我们向这一行和其他行添加更多按钮时,按钮将自动对齐并缩放大小。
默认情况下,按钮附带一个图像组件,边缘为圆角。出于美观目的,这不符合我们的场景。我们可以通过以下步骤移除它:
-
点击图像组件右上角的三个点图标。
-
从下拉列表中选择移除组件。
按钮不再有任何颜色。
接下来,我们将在这个游戏对象中填充五个游戏对象。简要来说,它们的名称和属性如下:
-
轮廓:为按钮添加边框 -
backPanel:按钮未选中时的颜色 -
selection:按钮选中时的颜色 -
powerUpimage:按钮上的图片 -
itemText:成本或售罄信息
以下截图显示了所有这些游戏对象组合在一起创建我们的新商店按钮:

图 9.42 – 一个商店按钮
小贴士
另一种改变按钮状态的方法是使用 Unity 的Button脚本,请参阅docs.unity3d.com/2017.3/Documentation/Manual/script-Button.html。
添加轮廓游戏对象
让我们先为我们的新商店按钮添加一个轮廓游戏对象:
-
在层次结构窗口中右键单击
00游戏对象,并从下拉菜单中选择UI | 图像。 -
选择
Image游戏对象,在outline中右键单击它。 -
在层次结构窗口中选择
outline,并更新其矩形变换和图像****颜色字段到以下:

图 9.43 – 轮廓矩形变换属性设置和图像组件颜色和透明度值
商店按钮现在将有一个彩色的轮廓。现在,让我们继续并看看按钮的backPanel。
添加 backPanel 游戏对象
让我们将backPanel添加到00游戏对象中:
-
在
00游戏对象中,从下拉菜单中选择UI | 图像。 -
右键单击新创建的
Image游戏对象,并将其命名为backPanel。 -
在选择
backPanel的情况下,在检查器窗口中更改其矩形变换,使其具有以下值:

图 9.44 – backPanel 矩形变换属性设置
在选择backPanel游戏对象的情况下,我们可以将backPanel游戏对象的设置更改为40,39,36,255。这是我们应用的第二个游戏对象,它给出了我们的默认颜色。
我们将在下一个步骤中将selection游戏对象添加到00游戏对象中。
添加选择游戏对象
要创建选择按钮,遵循上一节中提供的相同步骤。然而,请注意有三个不同之处:
-
将此游戏对象命名为
选择。 -
给出
144,0,0,255。 -
创建并应用一个
Selection。信息
我们在 第二章 中介绍了创建和应用标签,添加和操作对象。
以下截图显示了 selection 游戏对象的 Tag 和 Rect Transform 属性值:

图 9.45 – selection Rect Transform 属性设置
那是我们应用到 00 游戏对象上的第三个游戏对象。我们的按钮将在购买或按下不同按钮之前保持红色并亮起。我们将在下一个步骤中将 powerUpImage 游戏对象添加到 00 游戏对象中。
添加 powerUpImage 游戏对象
要创建 powerUpImage 按钮,按照上一节中提供的相同步骤操作,但需要进行以下三个更改:
-
将此游戏对象命名为
powerUpImage。 -
将
powerup精灵拖放到 Image 组件的 Source Image 字段中。 -
打勾 Preserve Aspect 复选框。
那是我们用于显示每个按钮图标的第四个游戏对象。
我们将在下一个步骤中将 itemText 游戏对象添加到 00 游戏对象中。
添加 itemText 游戏对象
要将 itemText 游戏对象添加到我们的 00 游戏对象中,请按照以下步骤操作:
-
在
00游戏对象中,从下拉列表中选择 UI,然后选择 Text。 -
右键单击新创建的
Text游戏对象,并将其命名为itemText。 -
在
itemText仍然被选中的情况下,在Inspector窗口中更改其 Rect Transform 和 Text 组件,使其具有以下属性:

图 9.46 – itemText Rect Transform 属性设置和 Text 组件值
那是我们需要添加到武器升级按钮中的第五个也是最后一个游戏对象。
在 00 游戏对象中,应按照以下截图中的顺序排列。如果顺序不同,只需单击并拖动其中一个到正确位置:

图 9.47 – 00 游戏对象及其在 Hierarchy 窗口中的子对象
在本节中,我们移除了旧的 shop 场景设置,其中我们使用射线投射系统在商店中选择物品。我们用带有 Button 组件的 2D 界面替换了旧的选择网格。这些按钮与 Unity 的水平 Vertical Layout Group 组件分组。这两个组的好处是,如果我们向网格中添加更多或更少的按钮,按钮将自动重新组织其位置和缩放。
我们需要对原本附加到每个游戏对象按钮上的 ShopPiece 脚本进行一些细微的修改。
一旦应用并修改了脚本,我们将检查按钮在新选择网格中的外观。
应用和修改我们的商店脚本
让我们简要回顾一下ShopPiece脚本的目的。选择网格中的每个按钮都将从脚本化对象中获取信息,以自定义按钮的名称、描述、价值和图像。由于按钮已从 3D 资产变为 2D,我们需要修改和添加一些代码以使其工作。
要修改ShopPiece以使其与我们的新 2D 按钮兼容,请执行以下操作:
-
在
Assets/Script文件夹中。 -
双击
ShopPiece脚本以打开文件。
第一行代码将允许我们的新代码从00游戏对象中获取引用。
-
在
ShopPiece脚本的顶部输入以下代码块:using UnityEngine.UI;
需要进行的第二次修改是替换Awake函数的内容。原始代码访问SpriteRenderer,用于访问每个多边形按钮上的精灵。我们正在替换的另一段代码应用于TextMesh组件,该组件用于显示 3D 文本。
-
要更新我们的
Awake函数,选择Awake()函数内的代码并将其删除。我们的Awake()函数应如下所示:void Awake() { }
我们现在可以进入第一个if语句,该语句将我们的可脚本化对象图标图像应用到按钮的图像上。
-
在
Awake()函数中,添加以下if语句:if (transform.GetChild(3).GetComponent<Image>() != null) { transform.GetChild(3).GetComponent<Image>(). sprite=shopSelection.icon; }
if语句从00按钮的第二个子对象中获取引用,并检查它是否具有Image组件。如果它有(应该是有的),我们将脚本化对象图标应用到它上。
-
另一个
if语句更新了按钮的文本。在Awake()函数中,紧接第一个if语句之后,添加以下代码块:if(transform.Find("itemText")) { GetComponentInChildren<Text>().text = shopSelection.cost.ToString(); }
if语句确保00按钮有itemText(应该是有的)。当找到itemText游戏对象时,其Text组件将接收武器脚本化对象的价格。
-
保存脚本。
-
在 Unity 编辑器中,选择层次结构中的
00游戏对象,并点击添加组件按钮。 -
在下拉列表中开始键入
ShopPiece,直到你看到它。当你看到它时,选择它。 -
在
ShopPiece组件中,仍然选择00游戏对象。 -
从列表中选择任何武器升级脚本化对象。
以下截图显示了应用了脚本化对象的ShopPiece脚本:

图 9.48 – ShopPiece 脚本,在其 Shop Selection 字段中持有 Shot_PowerUp 可脚本化对象
我们现在可以检查我们的按钮在应用了四个游戏对象及其修改后的ShopPiece脚本后的样子。
在接下来的几节中,我们将复制一系列新的商店按钮。这些商店按钮将自动适应我们放置的分配的游戏对象空间。然后,我们将清理任何旧的 UI 并用我们的新界面替换它。最后,我们将从代码中注释掉旧的射线投射系统并添加我们的新界面代码。
检查按钮的结果
在本节中,我们将审查 gridTop 游戏对象中的新 00 按钮。按钮太大,横跨了大多数 Canvas,如下面的截图所示:

图 9.49 – 00 游戏对象目前位于 gridTop 游戏对象中
但如果在 层次结构 窗口中选择 00 游戏对象并按 Ctrl (Mac 上的command*) 和 D 几次复制游戏对象,按钮将平均分割,如下面的截图所示:

图 9.50 – 三个 00 游戏对象看起来不那么拉伸
按钮可以很好地分割,并且可以在(Play Mode)之外重复多次以填充顶部和底部网格。要填充并命名网格,请执行以下操作:
-
在
00游戏对象中,按 Ctrl (Mac 上的command*) 和 D 三次。 -
分别将三个新复制的游戏对象重命名为
01、02和03。 -
在 检查器 窗口中,选择
01。 -
从列表中选择不同的可脚本对象来更改武器升级。
-
在
ShopPiece组件中,选择游戏对象02并选择不同的武器。
现在,我们需要用按钮填充底部行。为此,请按照以下步骤操作:
-
从
gridBottom游戏对象中点击并拖动03游戏对象。 -
在选择
03的情况下,按 Ctrl (Mac 上的command*) 和 D 两次。 -
将我们新创建的游戏对象重命名为
04和05。
以下截图显示了填充了顶部和底部的行:

图 9.51 – 六个商店按钮
由于我们商店中再也没有可售物品,所以底部三个按钮看起来很奇怪,因此让我们用一些售罄标志来替换这些按钮。这可以通过我们的可脚本对象资源轻松实现。
要为商店底部的行创建一个售罄标志,我们需要执行以下操作:
-
在
Assets/ScriptableObject中,在空白区域右键单击,然后选择 创建 | 创建商店物品。 -
将
Create Shop Piece文件重命名为SoldOut。 -
选择
SoldOut并赋予以下属性值:

图 9.52 – SoldOut 可脚本对象及其输入的值以及售罄精灵
最后,在检查器窗口的Shop Piece组件字段的Shop Selection中应用SoldOut文件到游戏对象03、04和05。
现在,我们需要为我们的广告和开始按钮重复执行类似的过程。
创建广告和开始按钮
要重新创建广告按钮,在层次结构窗口中选择我们复制的任一按钮,并执行以下操作:
-
按 Ctrl (Mac 上的 command) 和 D 复制另一个按钮,并将其拖动到层次结构窗口中的
gridOther游戏对象中。 -
将复制的游戏对象重命名为
AD。 -
因为
AD游戏对象不需要powerUpImage,我们可以删除它。 -
通过单击
itemText按钮名称左侧的箭头展开AD游戏对象。 -
在检查器窗口中将以下设置应用于文本组件:

图 9.53 – itemText 矩形变换属性设置和文本组件值
- 重复执行类似的过程,为
itemText和selection游戏对象组件(selection十六进制颜色:FFC300FF)执行以下操作,如图所示:

图 9.54 – itemText 文本组件值和选择图像组件属性值
以下截图显示了gridOther游戏对象及其内容,包括两个按钮:

图 9.55 – 在层次结构窗口中包含 AD 和 START 按钮及其子对象的 gridOther 游戏对象
现在我们完成了选择网格视觉效果,我们可以继续到描述面板,并将其部分从 3D 转换为 2D。
添加 BUY? 按钮
要将 2D BUY? 按钮添加到描述面板,请执行以下操作:
-
在层次结构面板中右键单击
Canvas游戏对象,然后从下拉列表中选择UI,接着选择按钮。 -
右键单击新创建的
Button游戏对象,并从下拉列表中选择重命名。 -
将
Button游戏对象重命名为BUY?。 -
在层次结构窗口中仍然选中
BUY?按钮,将其矩形变换属性设置为以下截图所示:

图 9.56 – BUY? 矩形变换属性设置
现在,BUY?按钮已经就位并且缩放正确,我们可以改变图像和按钮组件的美观。在图像组件中,选择远程按钮作为源图像字段,并从列表中选择无以移除按钮的曲线边缘。
接下来,我们将使BUY?按钮在按钮组件中高亮显示和按下时改变颜色。按照以下步骤操作:
-
在
255,0,0,255。 -
选择
255,195,0,255。 -
当光标移到
BUY?按钮上时,它将变成黄色,按下时将变成红色。
最后,对于 BUY? 按钮,我们需要修改其 Text 组件,如下所示:
-
在
BUY?按钮中展开它。 -
然后,选择
BUY?游戏对象的子对象,称为Text。 -
在 Inspector 窗口中,为
Text游戏对象的 Text 组件输入以下值:

图 9.57 – BUY? 游戏对象的 Text 组件属性值
以下截图显示了我们的 BUY ? 按钮的位置和样式:

图 9.58 – 现在,BUY? 游戏对象应该看起来像这个截图中的样子
在本节中,我们为我们的按钮应用了 Unity 的不同状态设置,而没有添加任何额外的代码。接下来,我们将添加一个简单的矩形图像来替换多边形四边形。
替换我们的 textBoxPanel 游戏对象
在上一节中,我们将我们的 BUY ? 按钮改为 2D,并且 BUY ? 按钮的一部分现在将被移动、缩放并调整到屏幕的比率,而不是保持静态。因此,我们面临的风险是 BUY ? 按钮可能会移动到它所在的静态 textBoxPanel 之外,如下面的截图所示:

图 9.59 – 解决 textBoxPanel 游戏对象与 BUY ? 按钮对齐的问题
此外,PlayerShipBuild 脚本有一个对 textBoxPanel 的引用,因此我们不能在不更改代码的情况下删除游戏对象。为了解决这个困境,我们可以移除 textBoxPanel 的 3D 组件,使其成为一个空的游戏对象,以容纳其中的其他游戏对象。
要从 textBoxPanel 游戏对象中移除组件,请执行以下操作:
-
在搜索栏中的
textBoxPanel中搜索,直到它出现。 -
选择
textBoxPanel并在 Inspector 窗口中通过选择并点击三个点,选择 移除组件 来移除 Quad (Mesh Filter) 和 Mesh Renderer 这两个组件。 -
要在 Hierarchy 窗口中恢复完整游戏对象的内容,请点击其窗口顶部的十字,位于搜索栏的右侧。
以下截图显示了两个三点图标的位置:

图 9.60 – Quad 和 Mesh Renderer 组件的远程按钮位置
现在,我们可以创建一个 2D 面板游戏对象来替换 textBoxPanel 游戏对象的视觉效果,如下所示:
-
在
Canvas游戏对象中,选择 UI,然后从下拉列表中选择 Image。 -
选择新创建的游戏对象,右键单击它,并从下拉列表中选择重命名。
-
将游戏对象重命名为
panel。 -
将
BUY?游戏对象移至面板游戏对象下方,在 Scene 窗口中,BUY?按钮位于panel顶部。以下截图显示了两个游戏对象的顺序:

图 9.61 – 在 Hierarchy 窗口中放置 BUY? 游戏对象
- 在选择
panel游戏对象的情况下,在 Inspector 窗口中为其 Rect Transform 设置以下值:

图 9.62 – 面板 Rect Transform 属性设置
我们可以通过在 Inspector 窗口中点击 panel 游戏对象的 Image 组件内的 颜色 字段,并给出以下截图中的突出显示值来更改 panel 游戏对象的颜色:

图 9.63 – 将面板游戏对象 Image 组件颜色值更改为此截图中的值
最后,我们可以修改我们的 textBoxPanel 和 bank 余额字体,使它们与商店按钮相匹配。
要修改我们的银行余额,我们需要执行以下操作:
-
在
bankText游戏对象中。 -
选择
bankText,在 Inspector 窗口中更新其 Text Mesh 组件,使 字体 字段采用我们新的 ethnocentric rg it 字体。 -
将
255,0,0,255更改为)。 -
在文本字段中添加几个数字以检查结果,如图下截图所示:

图 9.64 – bank 游戏对象字体更新
要更改我们的 textBoxPanel,我们需要做类似的事情。在这里,我们将选择其 name 和 desc 下的两个子游戏对象,并更新它们的 Text Mesh 组件如下:
-
添加
name和desc游戏对象的 Text Mesh 字体字段。 -
给它们一个白色 颜色。
-
在
name游戏对象的 Text Mesh Text 字段中添加以下内容:officer: -
将相同的更改应用到
desc游戏对象,但在 Text 字段中添加以下文本:will you need any upgrades before launch?
以下截图显示了 Game 窗口及其更新的字体:

图 9.65 – 我们的 textBoxPanel 游戏对象及其内容更新
现在,我们商店的所有视觉元素都已更新,并将支持各种屏幕比例。通过这样做,我们还引入了 Unity 自带的 Button 组件。
我们现在已经到达了可以打开 PlayerShipBuild 模板脚本的程度,该脚本位于我们章节的项目文件文件夹中。这个脚本将是当前我们一直在制作的 PlayerShipBuild 脚本的复制品,但我们将添加突出显示的代码以支持我们的 shop 场景的功能。
升级 PlayerShipBuild 脚本
在本节中,我们将用本章项目文件文件夹中的脚本替换当前的PlayerShipBuild脚本。替换脚本将包含与当前脚本相同的代码,但会包含逐步添加和删除的代码。
在我们开始编写新的替换脚本之前,让我们将当前的PlayerShipBuild脚本重命名为其他名称。要重命名当前的PlayerShipBuild脚本,请执行以下操作:
-
在
Assets/Script文件夹中。 -
双击
PlayerShipBuild脚本。 -
在打开
PlayerShipBuild脚本的情况下,将脚本顶部附近的名字从PlayerShipBuild重命名为PlayerShipBuild_OLD。 -
保存脚本并返回到项目窗口中的
Assets/Script文件夹。 -
慢慢点击两次
PlayerShipBuild脚本,以便提供重命名文件名的选项。 -
将文件名更改为
PlayerShipBuild_OLD。
现在,我们需要将PlayerShipBuild_OLD脚本从shop游戏对象中断开连接。
-
在搜索栏中输入
shop,直到你看到shop游戏对象。当你看到时,选择它。 -
在选择
shop游戏对象的情况下,点击PlayerShipBuild_OLD组件(不是变换)中的三个点。 -
从下拉列表中选择移除组件。
这样,我们就已重命名并从场景中断开了脚本。现在,我们可以从本章的项目文件文件夹中引入新的副本PlayerShipBuild脚本。
要将项目文件文件夹中的新副本PlayerShipBuild脚本连接起来,请执行以下操作:
-
在
Assets/文件夹中。 -
从文件夹中选择
PlayerShipBuild_NEW.txt脚本并将其拖到Assets/Script文件夹中。重命名它及其文件格式从.txt到.cs。这将用以下截图所示的PlayerShipBuild.cs替换一些我们旧的具有相同名称的射线脚本:

图 9.66 – 将 PlayerShipBuild_NEW 重命名为项目 Assets/Script 文件夹
我们现在可以将这个副本脚本应用到场景中的shop游戏对象上。让我们开始吧:
-
在层次结构窗口中选择
shop游戏对象,就像我们之前做的那样。 -
点击
PlayerShipBuild。当你看到PlayerShipBuild脚本时,从下拉列表中选择它。 -
在
shop游戏对象仍然被选中的情况下,我们现在可以配置附加的PlayerShipBuild脚本。 -
要配置脚本,设置
3,点击每个字段右侧的每个远程按钮,并从层次结构窗口中添加以下高亮显示的内容:

图 9.67 – 将以下三个游戏对象添加到 PlayerShipBuild 脚本的字段
我们的新 PlayerShipBuild 脚本现在已经就位。这意味着我们现在可以打开脚本,检查并揭示代码的新部分,同时解释旧代码移除的基本部分。
每个以下 移除旧… 子部分将执行以下操作:
-
//REMOVE(number):指代我们正在讨论的代码部分。
-
PlayerShipBuild脚本及其代码已被移除。 -
替换:之前代码被替换的内容。
移除旧商店场景的代码
在本节中,我们将遍历新安装的 PlayerShipBuild 脚本,并审查我已注释掉的部分代码,这样在 Unity 编译和执行时就不会被认可。
我们将关闭射线投射 3D 对象的能力,这是我们在 第五章 中编写的,为我们的游戏创建商店场景。因为我们已经将可交互的游戏对象从 3D 转换为 2D,所以我们不再需要射击和识别游戏对象,因为 Unity 将使用其自己的 按钮 组件来处理这一点。
要审查我们已注释的代码,请转到位于我们离开位置的 PlayerShipBuild 脚本(Assets/Script)。
信息
注释、注释和 取消注释 是指代码前有两条斜杠的情况。当我们的代码被编译器读取时(当我们运行代码时),这些将被忽略。
我们将分部分审查每段代码,以便清楚地知道我们将在 PlayerShipBuild 中应用哪些更改。
审查代码 – REMOVED 01
每个主要代码块都以 //REMOVED 开头,后跟一个数字。以下是为什么我们移除了 //REMOVED 01 的特定代码块的原因:
-
//REMOVED 01: 这段代码创建了一个射线投射并返回一个名为target的游戏对象。 -
移除原因:我们不再需要为每个我们射出射线的目标游戏对象获取引用。
-
OnClick事件,通常用于在选中时加载一个方法。
让我们继续向下滚动 PlayerShipBuild 脚本,直到我们到达 //REMOVED 02。
审查代码 – REMOVED 02
在本节中,我们将审查在 //REMOVED 02 中注释掉的内容:
-
//REMOVED 02: 这段代码将从射线投射选定的游戏对象中获取引用,并打开该selection游戏对象以显示已进行选择。 -
移除原因:该游戏对象除了起到装饰作用外,没有其他任何好处。
-
selection游戏对象。
让我们继续向下滚动 PlayerShipBuild 脚本,直到我们到达 //REMOVED 03。
审查代码 – REMOVED 03
在本节中,我们将审查在 //REMOVED 03 中注释掉的内容:
-
//REMOVED 03: 这部分代码检查玩家是否按下了射击按钮;如果是,代码将射出射线投射以检查是否与碰撞器接触。 -
if语句。 -
OnClick事件系统保存对所选游戏对象的引用。
让我们继续向下滚动 PlayerShipBuild 脚本,直到我们到达 //REMOVED 04。
审查代码 – REMOVED 04
在本节中,我们将回顾我们在 //REMOVED 04 中注释掉的内容:
-
//REMOVED 04: 此脚本检查射线投射游戏对象的名称。一旦通过一系列if语句识别,它就会运行适用于它的方法。 -
移除原因:这段代码会检查我们的射线投射与哪些特定名称接触。现在我们不再使用射线投射系统。
-
替换方案:每个按钮都有自己的事件触发器,运行它自己的方法。
让我们继续向下滚动 PlayerShipBuild 脚本,直到我们到达 //REMOVED 05。
审查代码 – REMOVED 05
在本节中,我们将回顾我们在 //REMOVED 05 中注释掉的内容:
-
//REMOVED 05: 在每一帧,它会检查玩家是否在商店中做出了选择。 -
Update方法。 -
替换方案:事件触发系统。
在前面的章节中,我们回顾并修改了与旧商店场景的射线投射系统交互的方式。
下一个阶段是将可以在按下“商店”场景中的按钮时通过事件直接调用的方法应用到我们的脚本中。
向我们的 PlayerShipBuild 脚本添加方法
在本节中,我们将构建两个主要部分,以便我们可以为 2D UI 选择设置我们的脚本。幸运的是,我们已经为这一章做了大部分工作,剩下的只是将脚本的部分设置为 public,以便我们的代码可以从其他来源访问,即我们的事件触发器(OnClick())。
我们将要做的第二件事是让我们的 AttemptSelection 方法接收游戏对象按钮,以便它将替换之前的 target 游戏对象。
为了确认这一点,target 游戏对象最初被用来存储来自我们的射线投射系统的射线击中。如果您想了解更多关于射线投射系统,请查看第五章,为我们的游戏创建商店场景,如果这听起来有些模糊。
首先,让我们让 PlayerShipBuild 脚本的方法对其他类可访问:
信息
默认情况下,我们的方法/函数和类的可访问级别设置为私有,除非另有说明。有关可访问级别的更多信息,请参阅以下链接:docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/accessibility-levels。
-
打开
PlayerShipBuild脚本,并将以下方法的public添加到其中:-
void WatchAdvert() -
void BuyItem() -
void StartGame()
-
这些方法现在可以通过 AttemptSelection 方法对其他脚本和 Unity 编辑器开放。
AttemptSelection 方法将像 public 方法一样被处理,但它现在还将接受一个游戏对象作为参数,这将是我们脚本附加到的按钮。
-
滚动到
AttemptSelection方法并添加一个public可访问级别,包括一个具有引用名称buttonName的游戏对象:public void AttemptSelection(GameObject buttonName) {
在这个 AttemptSelection 方法内部,我们检查 buttonName 而不是之前通过检查 target 来做,然后我们遵循相同的程序关闭任何突出显示的按钮,然后将 buttonName 游戏对象引用应用到另一个名为 tmpSelection 的游戏对象上,该对象最初是在 Select 方法中设置的。
-
使用以下代码更新
AttemptSelection:if (buttonName) { TurnOffSelectionHighlights(); tmpSelection = buttonName;
在我们的方法中继续下一行代码,我们将按钮的子 selection 游戏对象设置为活动状态(打开它)。以下截图显示了 Hierarchy 窗口中 selection 游戏对象的子编号:


![图 9.69 – 在 Hierarchy 窗口中选择 00 游戏对象
- 选择
00后,在 Inspector 窗口中向下滚动,直到你遇到 Button 组件。在 On Click () 面板中,点击以下截图所示的 + 图标:
![图 9.70 – 在按钮组件中点击 + 按钮以添加 On Click () 事件]

图 9.70 – 在按钮组件中点击 + 按钮以添加 On Click () 事件
-
将
shop游戏对象应用到当前显示为None (Object)的字段。 -
将
shop游戏对象从层次结构窗口拖动到None (Object)字段,如图下截图所示:
![图 9.71 – 将 shop 游戏对象从层次结构拖动到 00 游戏对象按钮的 On Click () 字段]
![图 9.71 – 将 shop 游戏对象从层次结构拖动到 00 游戏对象按钮的 On Click () 字段]
图 9.71 – 将 shop 游戏对象从层次结构拖动到 00 游戏对象按钮的 On Click () 字段
00游戏对象。现在,我们需要指定从00加载哪个函数。
当我们轻触/点击shop场景中的任一按钮时,我们将通过发出请求来调用AttemptSelection方法。
要使我们的00按钮加载AttemptSelection方法,请执行以下操作:
- 点击
PlayerShipBuild脚本和AttemptSelection(GameObject)公共方法,如图下截图所示:
![图 9.72 – 在字段中有 shop 游戏对象时,选择 PlayerShipBuild 脚本然后选择 AttemptSelection 函数]

图 9.72 – 在字段中有 shop 游戏对象时,选择 PlayerShipBuild 脚本然后选择 AttemptSelection 函数
在AttemptSelection中添加的最后一个字段。
- 点击最右侧的遥控按钮,并输入我们已选择的游戏对象,即
00。当你在列表中看到它时,点击它。
因此,当玩家按下00按钮时,我们的PlayerShipBuild脚本将从shop游戏对象中运行。然后,它将运行AttemptSelection方法,并将00游戏对象作为参数的引用。
- 设置
01和02。完成后,所有三个游戏对象的On Click()面板将如下所示:
![图 9.73 – 每个游戏对象(00、01、02)及其按钮组件的 On Click () 事件属性设置]

图 9.73 – 每个游戏对象(00、01、02)及其按钮组件的 On Click () 事件属性设置
对于我们的START和AD游戏对象按钮(位于层次结构窗口中),情况略有不同。
要使我们的AD和START游戏对象按钮在游戏中工作,请执行以下操作:
-
在检查器窗口中,将
shop游戏对象应用到AD游戏对象的OnClick事件,就像我们对最后几个游戏对象按钮所做的那样。 -
对
START游戏对象按钮执行相同的操作。 -
按照以下方式更新
START和AD游戏对象的OnClick事件:-
PlayerShipBuild,然后是WatchAdvert方法。 -
PlayerShipBuild,然后是StartGame方法。记住,如果你在列表中看不到它,请确保在PlayerShipBuild脚本中将其设置为public。你现在知道如何做了。如果不是,请检查Complete文件夹。
-
![图 9.74 – AD 和 START 游戏对象按钮的 On Click () 事件属性值]

图 9.74 – AD 和 START 游戏对象按钮的 On Click()事件属性值
最后要更改的按钮是层次结构窗口中的BUY?游戏对象按钮:
-
将通常的
shop游戏对象应用到其On Click()面板上。 -
将脚本设置为
PlayerShipBuild,然后是BuyItem。
注意,我们不会将事件监听器应用到我们的底部按钮行(已售罄),因为没有理由按下这些按钮。
我们现在可以测试商店场景了。保存场景,并在 Unity 编辑器中按下播放来尝试我们的新商店按钮。还值得在游戏窗口中测试不同的横向视图,以查看在选择了横向比率时 UI 按钮如何弹出形状。
以下截图显示了您必须遵循的更改比率的步骤。通过在 Unity 编辑器中点击游戏选项卡,然后从两个相当常见的比率中进行选择来完成此操作:

图 9.75 – 此截图显示了两个常见的比率标记
在本节中,我们重新评估了我们的代码,并移除了旧的射线投射系统,该系统涉及选择 3D 游戏对象来运行方法。我们用 Unity 的事件系统替换了它,包括动态组织了水平和垂直布局组组件的按钮组件。
现在,由于它支持不同的屏幕比率,UI 更加健壮。这将使我们的游戏与各种旧的和当前的移动和平板电脑屏幕以及尚未发布的便携式设备更加兼容。这有助于使我们的应用程序面向未来,避免出现任何令人尴尬的比率问题。
摘要
在本章中,我们探讨了游戏中的两个不同部分:游戏内 HUD 和重建我们的商店场景。这两个部分都使用了 Unity 的 UI 组件,但方式不同。
在游戏内 HUD 部分,我们研究了什么是 HUD 以及如何将其整合到我们的游戏中。通过这样做,我们学习了如何使用水平布局组正确排列图像,使用渲染纹理从第二个摄像头获取数据,以及使用原始图像显示从渲染纹理获取的数据。
最重要的是,根据 Unity 程序员考试的要求,你需要了解什么是 HUD 以及如何将其中的元素构建进去,例如小地图。
在本章的第二部分,我们回顾了当前商店场景的界面和代码。我们将其拆解并重新构建了其界面,作为一个 Unity 事件系统,它直接运行方法而不是投射射线来调用方法。我们还使界面支持多个比率。
使用本章涵盖的技能,你应该在审查和理解可以更高效地编写的代码时更有信心。
在下一章中,我们将继续我们的游戏内关卡制作,以便我们可以暂停游戏,手动添加和更改音乐和音效的音量,等等。
第十章:第十章:暂停游戏、调整声音和模拟测试
在本章中,我们将为我们的游戏添加背景音乐。然后,当关卡开始时,我们将使音乐淡入,当关卡完成时淡出,如果玩家死亡则停止。之后,我们将使用我们迄今为止学到的所有 UI 技能来创建一个暂停界面,并在其中添加一些滑动组件(这些组件将在下一章中用于音量控制)。在构建了暂停界面后,我们将通过冻结玩家、屏幕上的敌人、子弹和移动纹理来使游戏暂停。在暂停界面内,我们将给玩家提供继续游戏或退出的选项,以便游戏使用事件监听器回到标题屏幕,这些事件监听器我们在第九章中学习过,即创建 2D 商店界面和游戏内 HUD。最后,我们将提供一个包含 20 个问题的迷你模拟测试,涵盖我们从本章以及之前章节中学到的内容。
到本章结束时,我们将能够在脚本中直接修改AudioSource组件。我们将知道如何让每个 GameObject 在暂停界面中停止在屏幕上的移动。最后,我们将知道如何通过添加切换和滑动组件来创造更丰富的体验。
本章将涵盖以下主题:
-
应用和调整关卡音乐
-
创建暂停界面
-
添加游戏暂停按钮
-
模拟测试
在 Unity 程序员考试方面,下一节将列出本章将涵盖的核心目标。
本章涵盖的核心考试技能
以下是在本章中将涵盖的核心考试技能:
编程核心交互:
-
实现游戏对象和环境的行为和交互。
-
识别实现输入和控制的方 法。
开发应用程序系统:
- 应用程序界面流程,如菜单系统、UI 导航和应用设置。
场景与环境设计编程:
- 确定实现音频资源的脚本。
技术要求
本章的项目内容可以在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_10找到。
您可以在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition下载每个章节的项目文件的全部内容。
本章的所有内容都包含在本章的unitypackage文件中,包括一个Complete文件夹,其中包含本章我们将执行的所有工作。
观看以下视频,了解代码的实际应用:bit.ly/3kjkSBW。
应用和调整关卡音乐
在本节中,我们将探讨如何将背景音乐添加到我们的游戏关卡中。我们还将更新我们的脚本,以便在游戏的不同阶段改变音乐音量。
在接下来的章节中,我们将进行以下操作:
-
将音乐添加到每个关卡。
-
当玩家完成关卡时,让音乐淡出。
-
如果玩家死亡,让音乐立即停止。
-
确保音乐只在关卡场景中播放,不要在其他场景中播放。
因此,让我们开始,将游戏音乐添加到level1场景中。
更新我们的 GameManager 预制件
在本节中,我们将更新GameManager游戏对象,使其在AudioSource组件中包含一个新的游戏对象(称为LevelMusic)作为子对象,并播放 MP3 文件。这种设置对于简单游戏来说很理想;否则,我们可能会增加另一个管理器,这对于更大、更复杂的游戏来说才合适。
要创建一个游戏对象并将音乐文件添加到其中,我们需要做以下操作:
-
在 Unity 编辑器中,打开
bootUp场景(Assets /Scene)。 -
在层次窗口中右键点击GameManager,从下拉菜单中选择音频 | 音频源。
-
将新游戏对象重命名为
LevelMusic。以下截图显示了创建组件的游戏对象:


图 10.2 – 在音频源组件中将 lvlMusic MP3 添加到 AudioClip 字段
现在是保存我们的GameManager预制件的好时机,在层次窗口中选择它,然后在检查器窗口的右上角点击覆盖 | 应用所有。
如果我们现在点击level1场景,游戏将开始播放音乐。这是因为,默认情况下,音频源组件设置为唤醒时播放。这是好的,但它不会停止播放,直到场景改变,这对于大多数游戏来说已经足够了。然而,我们希望通过脚本添加对音乐音量的控制。
在下一节中,我们将更新ScenesManager脚本,并控制音乐何时以及如何播放。
为我们的游戏音乐准备状态
在本节中,我们将确保我们的游戏音乐不再设置为默认的ScenesManager脚本,因为它相对连接。
要添加我们的三个音乐状态(播放、停止和淡出),我们需要做以下操作:
-
在
ScenesManager脚本(Assets/Script)。 -
在
ScenesManager脚本顶部,我们定义变量的地方,就在我们的public enum Scenes属性的作用域下方,输入以下enum及其三个状态:public MusicMode musicMode; public enum MusicMode { noSound, fadeDown, musicOn }
我们在设置场景管理器脚本部分介绍了枚举,见第三章,管理脚本和进行模拟测试;其原理与标记状态相同。对于我们的enum,我们给它分配了一个数据类型名为MusicMode。
现在我们已经为三种状态贴上了标签,我们需要将这些状态付诸实践。我们需要让我们的三种状态执行它们预期的动作:
-
noSound: 没有音乐播放。 -
fadeDown: 音乐的音量将淡至零。 -
musicOn: 音乐将播放,并将音量设置为最大。
在游戏的各个阶段,我们希望触发这些状态,而访问这些短状态集的最佳方式是使用 switch case 来过滤出每个结果。
现在,我们需要为我们的三种音乐状态添加一个switch语句。
仍然在ScenesManager脚本中,我们将添加一个IEnumerator,它将对任一状态进行操作。我们已经在第二章的设置我们的敌人生成器脚本部分介绍了StartCoroutine/IEnumerator,添加和操作对象。
因此,因为我们添加了IEnumerator,所以我们也需要添加一个额外的库来支持此功能:
-
在
ScenesManager脚本的最顶部,添加以下库:using System.Collections; -
我们的脚本现在支持协程和 IEnumerators。
我将把我的IEnumerator放在Update函数的作用域之外,命名为MusicVolume,它接受MusicMode数据类型,我们将称之为musicMode:
IEnumerator MusicVolume(MusicMode musicMode)
{
-
在
MusicVolumeIEnumerator的作用域内,我们将从switch语句开始,并接收从musicMode引用发送过来的三个状态之一:switch (musicMode) { -
如果
musicMode包含noSound状态,那么我们使用GetComponentInChildren<AudioSource>()来获取唯一的包含AudioSource的子游戏对象,即新创建的LevelMusic游戏对象。 -
然后我们使用
Stop函数停止音乐,然后跳出 case:case MusicMode.noSound : { GetComponentInChildren<AudioSource>().Stop(); break; } -
下一个 case 是如果
musicMode包含fadeDown状态。在这里,我们获取LevelMusic游戏对象的引用,并随时间减少其volume值:case MusicMode.fadeDown : { GetComponentInChildren<AudioSource>().volume -= Time.deltaTime/3; break; } -
第三个也是最后一个情况是
musicOn;在 case 内部,我们首先检查是否已经将音频剪辑加载到了AudioSource中。如果没有音频剪辑,我们丢弃 case 的其余部分;否则,我们播放加载的音乐并将其设置为全音量(1为最高):case MusicMode.musicOn : { if (GetComponentInChildren<AudioSource>().clip != null) { GetComponentInChildren<AudioSource>().Play(); GetComponentInChildren<AudioSource>().volume = 1; } break;
为了关闭switch语句,我们添加一个带有几秒延迟的yield return,以便游戏有时间从switch语句中更改设置:
}
}
yield return new WaitForSeconds(0.1f);
}
现在我们已经创建了我们的 enum musicMode 状态,并在 IEnumerator 中设置了触发时每个状态将执行的操作,我们可以继续实现协程以更改音乐。
实现我们游戏的音状态
在本节中,我们将继续修改我们的 ScenesManager 脚本,并在代码的特定部分添加 StartCoroutines,使用 musicMode 状态,这是我们音乐音量将要改变的地方。例如,如果玩家在游戏中死亡,我们希望使用 noSound 状态立即停止音乐。
让我们从将音乐加载到游戏关卡开始,按照以下步骤操作:
-
在
ScenesManager脚本中,向下滚动到GameTimer方法。对于第一个情况,检查玩家是否在关卡 1、2 或 3 上,添加以下if语句:if (GetComponentInChildren<AudioSource>().clip == null) { AudioClip lvlMusic = Resources.Load<AudioClip> ("Sound/lvlMusic") as AudioClip; GetComponentInChildren<AudioSource>().clip = lvlMusic; GetComponentInChildren<AudioSource>().Play(); }
我们的 if 语句检查 LevelMusic 的 AudioSource 的音频剪辑是否为空(null)。如果没有音频剪辑,if 语句将执行以下操作:
-
从其文件夹中抓取我们的音频文件 (
lvlMusic.mp3) 并将其存储为AudioClip数据类型。 -
将音频剪辑应用于
AudioSource组件。 -
从
AudioSource运行Play函数。
现在我们启动关卡时音乐就会播放,我们需要确保在关卡完成时音乐能够淡出。这部分相当简单,因为我们已经找到了在关卡完成后淡出游戏音乐的正确方法。
向下滚动到 //if level is completed 注释并添加以下代码行,以便在关卡完成后淡出游戏音乐:
StartCoroutine(MusicVolume(MusicMode.fadeDown));
在 switch 语句中的最后一件事是添加一行代码,将音频剪辑重置为 null 作为安全措施:
default :
{
GetComponentInChildren<AudioSource>().clip = null;
break;
}
现在,如果调用 GamerTimer 方法,并且没有匹配的情况(我们的玩家不在关卡 1、2 或 3 上),那么玩家可能处于标题、游戏结束或启动场景,这意味着我们不会播放任何关卡音乐。
现在,我们将看看如何使用 StartCoroutines。
使用 StartCoroutine 与我们的音乐状态
现在,我们需要学习如何停止和开始音乐,通常是在关卡即将开始或突然结束(通常是在玩家死亡时)。仍然在 ScenesManager 中,回到需要更新的方法,以便它们可以支持音乐设置。按照以下步骤操作:
我们将要更新的第一个方法是 ResetScene。在方法的作用域内,输入以下代码:
StartCoroutine(MusicVolume(MusicMode.noSound));
这将调用 MusicVolume IEnumrator 来关闭音乐。以下代码块显示了更新后的 ResetScene 方法的外观:
public void ResetScene()
{
StartCoroutine(MusicVolume(MusicMode.noSound));
gameTimer = 0;
SceneManager.LoadScene(GameManager.currentScene);
}
我们将要更新的下一个方法是 NextLevel 方法。我们可以随时开始音乐,无论玩家在何处。我们可以使用以下代码随时播放:
StartCoroutine(MusicVolume(MusicMode.musicOn));
以下代码块显示了更新后的 NextLevel 方法的外观:
void NextLevel()
{
gameEnding = false;
gameTimer = 0;
SceneManager.LoadScene(GameManager.currentScene+1);
StartCoroutine(MusicVolume(MusicMode.musicOn));
}
现在,我们将继续到 Start 函数,它作为启动场景的安全措施,并查看它是否应该播放音乐。
当 ScenesManager 脚本处于活动状态时,它将自动尝试从我们的 LevelMusic 游戏对象的 AudioSource 组件播放音乐。
如果 AudioSource 不包含有效的 AudioClip(未找到 MP3),则我们的代码将假设玩家所在的关卡不需要音乐。
以下代码块展示了添加了 StartCoroutine 的 Start 函数的完整内容:
void Start()
{
StartCoroutine(MusicVolume(MusicMode.musicOn));
SceneManager.sceneLoaded += OnSceneLoaded;
}
最后一个需要更新的方法是 OnSceneLoaded。当一个关卡被加载时,我们将尝试打开音乐。下面的代码块展示了添加了 StartCoroutine 的 OnSceneLoaded 方法:
private void OnSceneLoaded(Scene aScene, LoadSceneMode aMode)
{
StartCoroutine(MusicVolume(MusicMode.musicOn));
GetComponent<GameManager> ().SetLivesDisplay(GameManager.
playerLives);
if (GameObject.Find("score"))
{
GameObject.Find("score").GetComponent<Text>().text =
GetComponent<ScoreManager>().PlayersScore.ToString();
}
}
保存脚本和 bootUp 场景。
我们为关卡场景编写的音乐操作代码已经完成。
在本节中,我们更新了 GameManager,使其包含一个名为 LevelMusic 的第二个游戏对象。这个 LevelMusic 游戏对象将包含一个 AudioSource 组件,当玩家开始关卡、完成关卡或通过 ScenesManager 脚本死亡时,可以对其进行操作。
在下一节中,我们将向游戏中添加一个暂停界面,并学习如何调整音乐和音效的音量,以及更多内容。
创建暂停界面
目前,我们无法暂停游戏,也没有一个允许我们操作游戏设置的选项屏幕。在本节中,我们将结合这些想法,使我们的游戏能够暂停,我们还将能够更改音乐和音效的音量。
在本节中,我们将执行以下操作:
-
在屏幕的右上角添加一个暂停按钮。
-
创建一个暂停界面。
-
添加继续游戏的功能。
-
添加退出游戏的功能。
-
添加音乐和音效的滑动条。
-
创建并连接 Audio Mixer 到两个滑动条。
暂停界面的最终效果可以在以下屏幕截图中看到:


图 10.3 – 暂停界面的最终视图
让我们从关注暂停界面的视觉效果开始。然后,我们将连接滑动条和按钮。
要开始处理暂停界面的视觉效果,我们需要做以下几步:
-
从
Assets/Scene加载level1场景。 -
当
level1场景加载后,我们现在可以专注于在 Hierarchy 窗口中创建一些用于暂停界面的游戏对象。 -
在 Hierarchy 窗口中右键单击
Canvas游戏对象,从下拉列表中选择 Create Empty。 -
选择新创建的游戏对象,右键单击它,选择
PauseContainer。
PauseContainer 现在需要调整到游戏屏幕的大小,以便这个游戏对象的任何子对象都可以调整到正确的缩放和位置。
- 要使
PauseContainer完全缩放到游戏屏幕的比例,确保在 Hierarchy 窗口中PauseContainer仍然被选中,并在 Inspector 窗口中将其 Rect Transform 属性设置为以下截图所示的属性:

图 10.4 – PauseContainer 矩形变换属性值
这样我们的 PauseContainer 就创建好了,并设置为包含两个主要游戏对象。第一个游戏对象将包含暂停屏幕的所有单个按钮和滑块。第二个游戏对象用于屏幕左上角的暂停按钮,它将使游戏暂停并弹出暂停控制。
以下截图显示了我们的游戏,其中暂停按钮位于屏幕左上角:

图 10.5 – 向我们的游戏添加暂停按钮
但在我们开始处理游戏中的暂停按钮之前,让我们先专注于暂停屏幕及其内容。为了创建一个包含游戏对象的 PauseScreen 游戏对象,我们需要对 PauseContainer 的 Rect Transform 属性重复类似的步骤。
要在 PauseContainer 中创建和包含一个 PauseScreen 游戏对象,请按照以下步骤操作:
-
在 Hierarchy 窗口中右键点击
PauseContainer游戏对象。 -
从下拉菜单中选择 Create Empty。
-
新创建的游戏对象将是
PauseContainer游戏对象的子对象。现在,让我们将新创建的游戏对象重命名为PauseScreen。 -
右键点击
PauseScreen。 -
在
PauseContainer中仍然选中PauseScreen。使用之前的 Rect Transform 图像作为参考。
现在,我们可以开始填充我们的 PauseScreen 游戏对象,添加其自身的游戏对象。
让我们开始降低屏幕亮度,这样当游戏暂停时玩家就不会分心。
要创建一个暗淡效果,请按照以下步骤操作:
-
在
PauseScreen游戏对象中选中blackOutScreen。 -
应用与最后两个游戏对象相同的 Rect Transform 属性。
-
现在,我们需要添加 Image 组件,以便我们可以用半透明的黑色覆盖屏幕。
-
在
blackOutScreen仍然被选中时,点击Image。一旦你看到blackOutScreen。 -
对于
blackOutScreen组件的图像属性,最后一步是将其 Color 设置为以下截图所示的值:

图 10.6 – 将 blackOutScreen 图像组件颜色值(RGBA)设置为截图中的值
现在,屏幕上将会出现一片半暗的屏幕。
现在,让我们添加 Pause 文本。为此,请按照以下步骤操作:
-
在
PauseScreen游戏对象中选中PauseText。 -
这次,给
PauseText的 Rect Transform 属性设置以下值:

图 10.7 – 将 PauseText Rect Transform 属性值设置为截图中所显示的值
接下来,我们需要添加PauseText游戏对象。
-
仍然选择
PauseText,点击Text直到你能在下拉列表中看到它。一旦做到这一点,就选择它。 -
将文本组件的设置更改为以下截图中所显示的设置:

图 10.8 – 将所有 PauseText 文本组件属性值更新为截图中所显示的值
如果你需要有关文本组件的更多信息,请查看第八章中的将文本和图像应用于你的场景部分,添加自定义字体和 UI。
以下截图显示了当前层次结构和场景视图的样式:

图 10.9 – 暂停文本应看起来像这样
我们已经自定义并居中了暂停标题。现在,让我们继续为音乐和效果音量设置添加一些滑块。我们将从音乐滑块开始,然后将其复制到屏幕的另一侧用于效果滑块。
将音量 UI 滑块添加到暂停屏幕
在本节中,我们将为暂停屏幕提供标题名称,并为游戏的音乐及其音效创建和自定义暂停屏幕音量滑块。
要创建、自定义和定位音乐滑块,请按照以下步骤操作:
- 在层次结构窗口中右键单击
PauseScreen游戏对象。然后,从下拉菜单中选择UI,接着选择滑块,如图所示:

图 10.10 – 从 UI 下拉菜单添加一个滑块
-
选择新创建的
Slider游戏对象,右键单击它,并将其重命名为Music。 -
接下来,通过更改其Rect Transform属性到以下截图中所显示的值来定位“音乐”滑块:

图 10.11 – 将音乐 Rect Transform 属性值设置为这里所示
我们现在将改变“音乐”滑块的条形颜色,使其更适合暂停屏幕。我们将将其从浅灰色改为红色。
要更改滑块的颜色,请执行以下操作:
-
点击
Fill Area游戏对象中Music游戏对象左侧的箭头。 -
从“音乐”游戏对象的下拉菜单中选择“填充”游戏对象,如图所示,就像它在层次结构窗口中看起来一样:
![图 10.12 – 在层次结构窗口的填充区域下拉菜单中选择填充
![Figure_10.12_B18381.jpg]
Figure 10.12 – 在层次结构窗口的填充区域下拉列表中选择填充
- 在仍然选择
Fill的情况下,在其检查器窗口中,将图像组件的颜色值更改为红色,如图所示以下截图:
![Figure 10.13 – 将填充图像组件的颜色值更改为本截图中的值
![Figure 10.13_B18381.jpg]
Figure 10.13 – 将填充图像组件的颜色值更改为本截图中的值
如果你仍然选择了Fill游戏对象,你可以通过调整检查器窗口中滑块组件底部的值滑块来查看滑块的红色背景,如图所示以下截图:
![Figure 10.14 – 将填充滑块组件的值更新到本截图中的值
![Figure 10.14_B18381.jpg]
Figure 10.14 – 将填充滑块组件的值更新到本截图中的值
此外,如前一个截图所示,我们需要设置滑块的-80和它的0。这样做的原因是,在下一章中,这些值将与音频混音器的相同值匹配。
音乐滑块的大小已经设置好了;我们只需要调整把手,使其不那么拉伸,更容易用手指点击或拖动。按照以下步骤进行操作:
-
在
Handle游戏对象中。然后,选择它。 -
在
Image组件中停止Handle游戏对象看起来那么拉伸。 -
在仍然选择
Handle的情况下,将所有轴上的3进行更改。
以下截图显示了我们的把手现在看起来是什么样子:
![Figure 10.15 – 滑块的把手现在已视觉更新
![Figure 10.15_B18381.jpg]
Figure 10.15 – 滑块的把手现在已视觉更新
音乐滑块现在已设置。这意味着我们可以继续到文本部分,以便我们可以为玩家标注滑块。为了给滑块添加自己的 UI 文本,我们需要执行以下操作:
-
在
PauseScreen游戏对象和下拉列表中,选择UI,然后选择文本。 -
右键单击我们新创建的
Text游戏对象并选择MusicText。 -
在仍然选择
MusicText游戏对象的情况下,将其矩形变换更改为以下值以定位和缩放文本到正确的位置:
![Figure 10.16 – 更改 MusicText 矩形变换的值
![Figure 10.16_B18381.jpg]
Figure 10.16 – 更改 MusicText 矩形变换的值
- 在检查器窗口中仍然选择
MusicText游戏对象,更新文本组件的值到以下属性值:
![Figure 10.17 – 更新 MusicText 文本组件属性值到本截图中的值
![Figure 10.17_B18381.jpg]
Figure 10.17 – 更新 MusicText 文本组件属性值到本截图中的值
我们的暂停屏幕开始成形。以下截图显示了我们目前拥有的内容:
![Figure 10.18 – 我们现在有了音乐音量滑块
![Figure 10.18_B18381.jpg]
图 10.18 – 现在我们有一个音乐音量滑块
现在,我们可以将我们的音乐文本和滑块复制并粘贴到屏幕的另一侧,并调整一些属性值,以便它被识别为音效音量条。
要复制并调整音乐文本和滑块,请按照以下步骤操作:
-
按 Ctrl (Command 在 Mac 上) 并从 Hierarchy 窗口中选择
MusicText和Music以便它们被突出显示。然后,按键盘上的 D 键来复制这两个游戏对象。 -
选择
Music (1)游戏对象,右键点击它,选择Effects。 -
选择
MusicText (1)游戏对象,右键点击它,选择EffectsText。 -
在
EffectsText仍然被选中时,在 Inspector 窗口中更新其 Rect Transform 属性,使用以下属性值:
![Figure 10.19 – 更新 EffectsText Rect Transform 属性值到本截图所示]
![img/Figure_10.19_B18381.jpg]
图 10.19 – 更新 EffectsText Rect Transform 属性值到本截图所示
- 在
EffectsText仍然被选中时,我们现在可以注意重命名文本。其余的 EffectsText 的MUSIC更改为EFFECTS,如以下截图所示:
![Figure 10.20 – 将 EffectsText Text Component 文本从 MUSIC 更改为 EFFECTS]
![img/Figure_10.20_B18381.jpg]
图 10.20 – 将 EffectsText Text Component 文本从 MUSIC 更改为 EFFECTS
接下来,我们可以将我们的 Effects 滑块移动到场景视图中 EFFECTS 文本下方。为此,请按照以下步骤操作:
- 在 Hierarchy 窗口中选择
Effects游戏对象。在 Inspector 窗口中,将它的 Rect Transform 属性更改为以下截图所示:
![Figure 10.21 – 更新 Effects Rect Transform 组件属性值]
![img/Figure_10.21_B18381.jpg]
图 10.21 – 更新 Effects Rect Transform 组件属性值
在视觉元素方面,我们的暂停屏幕几乎完成了。最后两件事是我们需要配置的 Quit 和 Resume 按钮。与滑块游戏对象一样,我们可以创建一个,复制并粘贴以创建第二个,然后编辑它们。
要创建和自定义一个 Quit 按钮,请按照以下步骤操作:
-
在 Hierarchy 窗口中右键点击
PauseScreen游戏对象,然后从下拉列表中选择 UI,然后选择 Button。 -
使用新创建的
Button游戏对象,我们可以将其重命名为Quit;在Button中右键点击Button游戏对象并将其重命名为Quit。 -
现在,我们可以将
Quit游戏对象放置到正确的位置,并在PauseScreen游戏对象内调整其大小。 -
在
Quit游戏对象仍然被选中时,在 Inspector 窗口中将其 Rect Transform 属性更改为以下截图所示:
![Figure 10.22 – 更新 Quit Rect Transform 属性值]
![img/Figure_10.22_B18381.jpg]
图 10.22 – 更新 Quit Rect Transform 属性值
- 现在,
Quit游戏对象将位于暂停屏幕的右下角:

图 10.23 – 我们的游戏退出按钮已添加到暂停屏幕
接下来,我们可以通过更改按钮的精灵、颜色和文本来自定义它。我们将从移除之前截图中所见的圆角按钮精灵开始。
在仍然选择我们的 Quit 按钮的情况下,我们可以通过以下步骤移除单个精灵:
-
在 检查器 窗口中,点击右上角的远程按钮(以下截图中的 1 所示)。
-
将出现一个新窗口。从下拉菜单中选择 无(以下截图中的 2 所示):

图 10.24 – 移除退出按钮的源图像精灵
接下来,我们将更改按钮的颜色,如下所示:
-
在 层次结构 窗口中仍然选择
Quit游戏对象,我们可以在 检查器 窗口中更改 按钮组件 的 正常颜色 属性。 -
选择标题为
255、0、0、150的颜色字段。
接下来我们需要对按钮做的下一件事是更改其文本。
-
在 层次结构 窗口中仍然选择我们的
Quit游戏对象,选择其名称左侧的向下箭头。 -
从
Quit游戏对象中选择Text子游戏对象,并在 检查器 窗口中的 文本 组件中设置以下属性设置:

图 10.25 – 将 Text 游戏对象的 Text 组件更新为截图所示的值
我们将得到一个看起来更适合我们游戏的按钮:

图 10.26 – 我们的游戏退出按钮现在看起来更适合我们的游戏
在本节中最后要做的就是复制我们刚刚创建的 Quit 游戏对象,并将文本重命名为 RESUME。Resume 按钮将用于取消暂停屏幕,让玩家继续玩游戏。
要创建 Resume 游戏对象,我们需要执行以下操作:
-
在 层次结构 窗口中选择
Quit游戏对象。 -
按下键盘上的 Ctrl 键(在 Mac 上为 command 键)和 D 键来复制游戏对象。
-
将复制的游戏对象从
Quit (1)重命名为Resume。 -
在仍然选择
Resume的情况下,在 检查器 窗口中更改其 矩形变换 属性值,如以下截图所示:

图 10.27 – 给 Resume 矩形变换赋予与截图相同的属性值
对于 Resume 游戏对象,剩下的只是将其文本从 QUIT 更改为 RESUME。通过在 Hierarchy 窗口中点击左侧的箭头来扩展 Resume 选择,遵循以下步骤:
-
在 Hierarchy 窗口中选择
Text游戏对象。 -
在
QUIT到RESUME,如图所示:

图 10.28 – 暂停按钮
暂停屏幕现在在视觉上已经完成,并且由于使用了我们的 Anchors(来自 Rect Transform 属性),可以支持各种屏幕比例。之前我们提到,我们将在游戏屏幕的左上角有一个暂停按钮,这样我们就可以暂停游戏并加载我们刚刚制作的暂停屏幕。
本节中我们所做的一切都是在 Unity 编辑器中完成的,没有使用任何代码。在本节中,我们涵盖了以下主题:
-
如何访问我们的暂停屏幕
-
暂停屏幕将如何覆盖我们游戏中的关卡
-
在暂停屏幕的背景上应用半透明的黑屏以降低游戏亮度
-
为我们的音乐和效果创建滑块
-
将自定义文本应用到各个点
-
使用 Unity 的 Button 组件为玩家提供退出或继续游戏的选择
现在,让我们制作暂停按钮。之后,我们可以开始查看如何将这些滑块和按钮与我们的代码连接起来。
添加游戏暂停按钮
在上一节的开头,我们简要地提到了 游戏内暂停按钮。这个按钮将在关卡开始时出现,一旦按下,玩家、敌人和已经发射的子弹将暂停时间。在本节中,我们只关注视觉效果,就像我们在上一节中处理暂停屏幕一样。
暂停按钮的行为将略不同于我们之前制作的按钮。这次,按钮将是一个 toggle 游戏对象,执行以下操作:
- 在 Hierarchy 窗口中选择
PauseContainer游戏对象,右键单击它,从下拉列表中选择 UI,然后选择 Toggle,如图所示:

图 10.29 – 添加一个 Toggle(开关)
-
在
PauseButton中仍然选择Toggle游戏对象。 -
目前,我们的
PauseButton看起来与我们想要的样子完全不同,就像下面的截图所示,它更像是一个复选框。然而,我们可以修复这个问题,让它看起来像一个正常的暂停按钮,但具有切换(开启或关闭)的功能:

图 10.30 – 我们将把复选框改为暂停按钮
要改变 PauseButton 游戏对象当前的外观,使其看起来像前面截图中的预期样子,我们需要执行以下操作:
- 在暂停按钮游戏对象中展开其内容,如下截图所示:

图 10.31 – 在层次结构窗口中展开所有暂停按钮子对象
- 在层次结构窗口中选择
Label游戏对象,并在键盘上按Delete键。
将切换标签删除。
- 接下来,我们将设置我们的游戏对象到正确的位置和缩放。在层次结构窗口中选择
PauseButton,并在检查器窗口中为其矩形变换设置以下属性:

图 10.32 – 更新暂停按钮矩形变换属性值
现在将切换放置并缩放到游戏画布的左上角,如下截图所示:

图 10.33 – 暂停按钮的锚点设置为屏幕的左上角
- 注意我们的暂停按钮游戏对象包含另一个名为
Background的游戏对象,但在层次结构窗口中没有选择其Background游戏对象:

图 10.34 – 从层次结构窗口中选择背景游戏对象
- 要纠正背景游戏对象的背景游戏对象,并在检查器窗口中设置以下值:

图 10.35 – 将背景矩形变换属性值设置为截图中的值
在锚点大小方面,背景游戏对象现在与暂停按钮游戏对象的大小相同。
我们现在可以开始调整大小并使用合适的图像填充背景。我们将用深色圆圈替换带有勾选标记的白色正方形图标。按照以下步骤进行操作:
-
如果尚未这样做,在背景游戏对象中展开
PauseButton。 -
在检查器窗口中的图像组件的右侧选择源图像的小遥控按钮。
-
从出现的下拉菜单中,用
UISprite替换其当前选择,并将其更改为旋钮。其选择在以下截图中显示。
现在正方形已经变成了圆形。现在,我们可以改变它的颜色,使其与游戏 UI 的其余部分相匹配。
- 在选择背景游戏对象的情况下,选择其
92、92、92和123,如下截图所示:

图 10.36 – 我们背景图像组件应具有与本截图所示相同的值
接下来,我们可以将灰色椭圆形形状变成圆形。
仍然在图像组件中,将图像类型设置为简单,并勾选保持纵横比框,如图中所示的前一个屏幕截图。
图像类型为图像提供不同的行为;例如,分割在进度条/计时器中表现良好,可以随时间增加可见图像的部分。
保持纵横比意味着无论图像如何缩放,它都将保持其原始形式 – 将不会有挤压或拉伸的外观。
这是场景视图中PauseButton的特写视图:

图 10.37 – 我们暂停按钮当前的外观
现在,我们需要将勾选图像替换为一个大型的暂停符号。按照以下步骤进行操作:
- 从
Background游戏对象中选择Checkmark游戏对象,并在检查器窗口中,为其矩形变换设置以下设置:

图 10.38 – 设置勾选矩形变换属性值到本截图所示
-
在
Checkmark中仍然选择Checkmark游戏对象,通过点击远程按钮并从下拉列表中选择暂停精灵来将其设置为pause。 -
选择
152、177、178、125。 -
如果尚未设置,将图像类型更改为简单。
-
勾选保持纵横比框,如图中所示的下个屏幕截图:

图 10.39 – 更新勾选图像组件属性值到本截图所示
场景窗口应该看起来像这样,我们的暂停按钮位于左上角:

图 10.40 – 我们暂停按钮现在视觉上完整
最后,为了确保当我们在它上点击时,toggle按钮实际上会执行某些操作,我们需要确保在我们的层级窗口中有一个EventSystem。这样做非常简单;按照以下步骤进行:
-
在层级窗口中,右键单击空白区域。
-
如果层级中没有EventSystem,请选择UI,然后选择EventSystem。
-
保存场景。
在本节中,我们在一个支持各种横版变体的屏幕上混合了我们的 UI 图像、按钮、文本和滑块。
在下一节中,我们将继续讨论当玩家按下按钮或移动滑块时,我们在暂停屏幕中制作的每个 UI 组件将执行的操作。
创建我们的 PauseComponent 脚本
PauseComponent脚本将负责管理与访问和修改暂停屏幕提供给玩家的条件相关的所有事情。在这里,我们将遵循一系列小节,这些小节将带我们设置PauseComponent脚本的各个部分。在我们这样做之前,我们需要创建我们的脚本。如果你不知道如何创建脚本,那么请回顾第二章中的设置我们的相机部分,添加和操作对象。一旦你完成了这个,将脚本重命名为PauseComponent。出于维护目的,在项目窗口中将你的脚本存储在Assets/Script文件夹中。
现在,让我们继续到PauseComponent脚本的第一个小节,通过应用游戏中的暂停按钮的逻辑。
暂停屏幕基本设置和暂停按钮功能
在本节中,我们将使暂停按钮在玩家在关卡中控制游戏时出现。当玩家按下暂停按钮时,我们需要确保所有移动组件和滚动纹理都冻结。最后,我们需要引入暂停屏幕本身。
如果我们以当前状态开始关卡,我们会看到PauseScreen游戏对象覆盖了屏幕。这看起来很棒,但我们需要暂时关闭它。要关闭PauseScreen游戏对象,请执行以下操作:
-
在 Unity 编辑器中,通过双击
Assets/Script中持有的文件来打开新创建的PauseComponent脚本。 -
在脚本打开后,在顶部添加
UnityEngineUI 库,以便为我们的代码提供额外的功能(操作文本和图像组件),包括常用的UnityEngine库和类的名称,以及其继承自MonoBehaviour:using UnityEngine.UI; using UnityEngine; public class PauseComponent : MonoBehaviour { -
将以下变量添加到
PauseComponent类中:[SerializeField] GameObject pauseScreen; }
[SerializeField]将使pauseScreen变量在public中可见。第二行是一个GameObject类型,它将存储对整个PauseScreen游戏对象的引用。
-
保存脚本。
-
回到 Unity 编辑器,从
PauseComponent中选择PauseContainer游戏对象,直到你在下拉列表中看到它。 -
现在,将
PauseScreen游戏对象从PauseScreen拖放到相应的位置,如下面的截图所示:


图 10.41 – 将 PauseScreen 游戏对象拖放到暂停组件 | 暂停屏幕字段
回到PauseComponent脚本,我们现在可以在关卡开始时关闭PauseScreen游戏对象,并在玩家按下暂停按钮时重新打开它。要关闭PauseScreen,我们可以执行以下操作:
-
在
PauseComponent脚本中,创建一个Awake函数,并在其中关闭pauseScreen游戏对象,如下面的代码所示:void Awake() { pauseScreen.SetActive(false); } -
保存脚本。
现在,当我们按下屏幕顶部的 播放 按钮时,我们可以在编辑器中测试它。游戏将运行,而不会显示暂停屏幕。现在,我们可以在关卡开始后的几秒钟内向玩家介绍暂停按钮。
让我们先创建一个方法,该方法将关闭/打开玩家暂停按钮的视觉效果和交互性:
-
返回到
PauseComponent脚本,并创建一个接受一个bool参数的方法,如下面的代码所示:void SetPauseButtonActive(bool switchButton) {
由于我们的 PauseComponent 脚本附加到 PauseContainer 游戏对象上,我们可以轻松访问任何游戏对象及其组件。附加的其他两个主要游戏对象是 PauseScreen 和 PauseButton。接下来我们将添加到 SetPauseButtonActive 的几行代码将与 PauseButton 游戏对象的视觉效果和交互性相关。
-
要更改我们
PauseButton的可见性,我们需要访问其Toggle组件的colors值并将其存储在一个临时的ColorBlock类型中。在SetPauseButtonActive方法中输入以下代码行:ColorBlock col = GetComponentInChildren<Toggle>().colors; -
接下来,我们需要通过查看方法接收到的
bool参数来检查值的条件。如果switchButton的bool设置为关闭,那么我们将所有与切换相关的颜色设置为全零,即黑色和零 alpha(完全透明)。
在我们之前输入的代码行之后输入以下代码:
if (switchButton == false)
{
col.normalColor = new Color32(0,0,0,0);
col.highlightedColor = new Color32(0,0,0,0);
col.pressedColor = new Color32(0,0,0,0);
col.disabledColor = new Color32(0,0,0,0);
GetComponentInChildren<Toggle>().interactable = false;
}
前面的代码显示我们运行了一个检查,以查看 bool 参数是否为 false。
- 如果
switchButton包含一个false值,那么我们将进入if语句并将col(暂停按钮的颜色)的normalColor属性设置为全零。这意味着它根本不会显示这个按钮。然后,我们将相同的值应用到暂停按钮的所有其他可能的颜色状态上。我们还需要将Toggle的interactable值设置为false,这样玩家就不会意外地按下暂停按钮。
下一个图中的左侧截图显示了我们刚刚输入的代码。右侧的截图是经过我们 if 语句更改属性的 Toggle 组件:

图 10.42 – 左侧的代码将操作右侧的 Toggle 属性值
如果 switchButton 设置为 true,我们将所有零的值设置为所选的颜色值,并使 PauseButton 不可交互。
-
在我们刚刚写的代码之后输入以下代码:
else { col.normalColor = new Color32(245,245,245,255); col.highlightedColor = new Color32(245,245,245,255); col.pressedColor = new Color32(200,200,200,255); col.disabledColor = new Color32(200,200,200,128); GetComponentInChildren<Toggle>().interactable = true; }
在此代码段之后的最后两行将 col 值重新应用到 Toggle 组件上。
第二行代码打开或关闭暂停符号。如果没有设置,那么暂停按钮将出现/消失,而不会影响两个白色的暂停条纹。
-
在前面的代码之后,添加了最后两个
GetComponentInChildren行,这会将颜色重新应用到Toggle组件,并使用switchButton变量将暂停符号设置为开或关:GetComponentInChildren<Toggle>().colors = col; GetComponentInChildren<Toggle>() .transform.GetChild(0).GetChild(0).gameObject.SetActive (switchButton); } -
现在,我们只需要使用我们刚刚编写的函数。最初,我们希望暂停按钮在关卡开始时不可见,直到玩家控制他们的飞船。要关闭暂停按钮,我们需要重新访问
Awake函数并执行以下操作:void Awake() { pauseScreen.SetActive(false); SetPauseButtonActive(false); Invoke("DelayPauseAppear",5); }
在这里,我在 Awake 函数中添加了两行额外的代码。SetPauseButtonActive(false) 使用我们刚刚创建的方法关闭暂停按钮,而 Invoke 函数将延迟 5 秒,直到我们运行 DelayPauseAppear 方法。在 DelayPauseAppear 中是 SetPauseButtonActive(true),这是我们玩家获得对飞船控制的时间。
-
在
Invoke函数中添加我们提到的额外方法来打开暂停按钮,如下所示:void DelayPauseAppear() { SetPauseButtonActive(true); } -
保存脚本。
在 Unity 编辑器中,按播放;我们的游戏将正常开始,5 秒后,暂停按钮将出现在左上角。如果我们按下暂停按钮,它将中断,不会发生任何额外的事情。这是因为我们没有在按下暂停按钮时让它执行任何操作。
让我们回到 PauseComponent 脚本,并添加一个可以在按下暂停按钮时运行的小方法。要添加一个暂停方法,该方法冻结游戏并显示我们之前构建的暂停屏幕,请按照以下步骤操作:
-
重新打开
PauseComponent脚本并输入以下方法:public void PauseGame() { pauseScreen.SetActive(true); SetPauseButtonActive(false); Time.timeScale = 0; } -
在
PauseGame方法中,我们设置以下内容:-
我们将暂停屏幕游戏对象的活跃度设置为
true。 -
关闭暂停按钮(因为我们有
QUIT按钮可以使用)。
-
-
将游戏的
timeScale设置为 0,这将停止场景中所有移动和动画对象。有关timeScale的更多信息,请查看官方 Unity 文档,docs.unity3d.com/ScriptReference/Time-timeScale.html。
timeScale 也可以在 Unity 编辑器的时间管理器中找到。它位于编辑器窗口的顶部,在编辑 | 项目设置 | 时间下。
你还有其他有用的属性,例如固定时间步长,你可以更改其值以使你的物理模拟更精确。有关时间管理器及其属性的更多信息,请查看以下链接:docs.unity3d.com/Manual/class-TimeManager.html。
- 保存脚本并返回到编辑器。
现在,我们需要将新的 PauseGame 方法附加到 PauseButton 事件系统,如下所示:
-
从层次结构窗口中选择
PauseButton游戏对象。 -
在检查器窗口的底部,点击加号(+)来添加一个事件:

图 10.43 – 向切换组件添加事件
-
接下来,将包含我们的
PauseComponent脚本的PauseContainer拖到空字段(从下拉菜单中标记为PauseComponent)。 -
最后,选择
PauseGame ()公共方法(在以下屏幕截图中标记为3)。
以下屏幕截图显示了我们在选择PauseGame ()方法中经历的标记步骤:



现在是尝试查看当我们按下暂停按钮时是否会出现暂停屏幕的好时机。在 Unity 编辑器中按下播放,在游戏窗口中,当它出现时,在左上角按下暂停按钮。暂停屏幕将出现;除非我们为我们的继续和退出按钮编写逻辑,否则我们将无法从中逃脱。
到目前为止,在本节中,我们已经赋予了玩家暂停游戏的能力。在下一节中,我们将使玩家能够在暂停屏幕中继续或退出游戏。
从暂停屏幕恢复或退出游戏
在本小节中,我们将通过添加两个方法继续扩展PauseComponent脚本:
-
继续 -
退出
让我们从添加继续按钮的逻辑开始;按照以下说明操作:
-
如果
PauseComponent脚本尚未打开,请转到Assets/Script。双击文件以打开它。 -
在
PauseComponent脚本内部,滚动到可以添加新方法的位置 – 不重要,只要它在PauseComponent类内部,并且不干扰其他方法即可。 -
现在,我们将添加一个
Resume方法,如果玩家希望关闭暂停屏幕,游戏动画将继续,左上角的暂停按钮将重新出现。要使所有这些发生,请添加以下代码:public void Resume() { pauseScreen.SetActive(false); SetPauseButtonActive(true); Time.timeScale = 1; }
此代码与上一节中显示的代码类似;只是顺序相反(值设置为 true,现在是 false,反之亦然,以恢复原始设置)。
-
保存脚本并返回到 Unity 编辑器。
-
在 Unity 编辑器中,选择
PauseScreen游戏对象。确保层次结构窗口也显示已选择继续。 -
在层次结构窗口中,将
PauseContainer游戏对象拖到无(对象)的点击()事件系统中。 -
选择
PauseComponent,然后为Resume游戏对象按钮设置正确的事件系统On Click ():



-
在继续到退出按钮之前,让我们先测试继续按钮。在编辑器中按播放。一旦暂停按钮出现在游戏窗口的左上角,点击它。
-
最后,点击大号继续按钮。我们将返回到正在进行的游戏。
-
我们暂停屏幕中要连接的最后一个按钮是
PauseComponent脚本,并将以下方法添加到脚本中:public void Quit() { Time.timeScale = 1; GameManager.Instance.GetComponent<ScoreManager>(). ResetScore(); GameManager.Instance.GetComponent<ScenesManager>(). BeginGame(0); }
我们刚刚输入的代码重置了游戏;timescale值回到1。我们直接从ScoreManager重置玩家的分数,并直接告诉ScenesManager带我们回到场景零,即我们的bootUp场景。
- 在结束本节之前保存脚本。
这与继续按钮在设置事件到我们的脚本方面相似。
-
从暂停屏幕中选择退出按钮,并确保在检查器窗口的底部,你遵循与继续按钮相同的步骤。
-
当我们开始应用退出方法时。
以下截图显示了Quit游戏对象的按钮设置:

图 10.46 – 当按下退出按钮时,它将运行退出函数
在我们结束本章之前,我们需要确保当游戏暂停时,玩家和敌人的行为符合我们的预期。
暂停玩家和敌人
因此,我们已经到达了可以按下游戏中的暂停按钮并观察游戏暂停的时刻的点。为了确保场景已保存,包括新的和编辑的脚本,让我们测试暂停屏幕:
-
在 Unity 编辑器中按播放。
-
当暂停按钮出现时,按下它。
-
游戏暂停了,但我们的敌人看起来像是漂浮离开了。此外,当我们按下玩家的射击按钮时,其玩家子弹光在船上闪烁。
让我们先修复敌人的漂浮问题。
-
这是一个简单的修复——我们需要更改
EnemyWave脚本的更新时间。 -
停止播放。然后,在
Assets/Script中双击EnemyWave脚本。 -
找到以下内容的行:
void Update()
更改为以下内容:
void FixedUpdate()
-
保存
EnemyWave脚本。更多信息
更多关于FixedUpdate的信息可以在这里找到:
docs.unity3d.com/ScriptReference/MonoBehaviour.FixedUpdate.html。
现在,让我们加强玩家的行为,以便在游戏暂停时冻结其所有功能。为了冻结我们的玩家,我们需要重新打开其脚本:
-
在
Assets/Script文件夹中。 -
双击
Player脚本。 -
滚动到
Update函数,并用以下if语句包裹玩家的Movement和Attack方法:void Update () { if(Time.timeScale == 1) { Movement(); Attack(); } }
上述代码会检查游戏的时间缩放是否以全速(1)运行,然后继续执行Movement和Attack方法。
- 保存
Player脚本。
太好了!我们现在有了暂停游戏、继续游戏或退出游戏的能力。不用担心将这个暂停屏幕添加到其他关卡中,因为我们在下一章中会这样做。说到下一章,在那里,我们将探讨如何更改 音乐 和 效果 滑块。现在,让我们回顾一下本章中我们所学到的内容。
摘要
通过完成本章,我们的游戏得到了进一步的改进,现在有了暂停屏幕,就像你从任何游戏中期望的那样。我们还学习了如何使用 timeScale 值在游戏中冻结时间。我们回顾了之前章节中的一些内容,例如事件监听器和 UI 定位和缩放,但我们还使用了其他 UI 组件,如切换和滑块,并将它们修改以适应我们的暂停屏幕。我们涵盖的其他内容还包括引入一些 MP3 音乐,并确保脚本知道何时淡入淡出和停止声音。
在你在这本书之外创建的下一个游戏中,你将不仅知道何时以及如何添加背景音乐以便在播放时播放,而且还将了解如何将你的音频附加到状态机。有了状态机,你可以使音乐在特定时刻播放、停止和淡出,例如当游戏屏幕暂停时。现在,你将能够使用本章中你学到的 UI 组件来创建你自己的菜单/暂停屏幕。通过这样做,你可以运行事件来关闭或恢复你的游戏。你还知道如何使用 timeScale 函数完全暂停你的游戏或减慢时间。
在下一章中,我们将探讨 Unity 的音频混音器,以控制玩家子弹和音乐的音量,并将其连接到我们的暂停屏幕音量滑块。我们还将探讨需要存储的不同类型的数据,例如游戏记住音量设置,这样我们就不必每次启动游戏时都调整滑块。
目前,我祝愿你在你的迷你模拟测试中一切顺利!
模拟测试
-
如果你想在
[Header]中保持一个私有变量可见, -
[SerializeField] -
[AddComponentMenu] -
[Tooltip]
- 你为移动设备创建了一个弹球游戏;游戏机制都运作良好,但你还需要应用一个暂停屏幕。显然,当玩家按下暂停时,整个游戏应该冻结。你将通过将 Unity 的
timeScale设置为零来实现这一点。
当我们将 Time.timeScale 设置为 0 时,哪个时间属性不受影响?
-
captureFramerate -
frameCount -
realtimeSinceStartup -
timeSinceLevelLoad -
在你的
BuildSettings窗口中有一个场景列表。你知道第一个场景是你的标题场景,其余的则是你的游戏关卡场景。你的游戏设计师还没有确定场景的名称,并且一直在更改它们。作为一个程序员,你可以通过使用SceneManager的哪个方法来选择要加载的场景?-
GetSceneByBuildIndex() -
GetActiveScene() -
SceneManager.GetSceneByName() -
SceneManager.GetSceneByPath()
-
-
如果你有一个可以启用或禁用的暂停屏幕,哪个是切换两个的最佳 UI 组件?
-
切换
-
Button -
Slider -
Scroll Rect
-
-
你收到了一个移动设备上的 Killer Wave 新原型进行测试。你注意到当你进入屏幕中间有 UI 文本的水平时,你可以移动船只或射击。是什么导致了移动的限制?
-
屏幕比例的比率未校准。
-
UI 文本已勾选 Raycast Target。
-
移动设备需要充电。
-
屏幕上同时有太多手指会混淆输入系统。
-
-
你创建了一个 UI 按钮,当你的账户中有钱时,它会显示硬币的图片,而当你的账户为空时,它会显示一个空空的棕色袋子。
在 Unity 检查器中,按钮的过渡字段应设置为哪个值以支持这些图像更改?
-
颜色色调
-
无
-
动画
-
精灵交换
-
当在屏幕底部输入一些 UI 详细信息以显示玩家的生命值和所在关卡时,你注意到需要文本具有特定的大小。你可以将文本更改为任何大小,但还需要适应屏幕的比率。
最好的方法来修改字体以确保它不会显得挤压?
-
减小字体大小。
-
打开最佳适配。
-
将垂直溢出设置为截断。
-
将水平溢出设置为溢出。
-
你开始制作一个游戏,该游戏依赖于时间暂停、倒退和快进,但仅限于你的敌人,利用
Time.timeScale功能。一些敌人没有受到时间变化的影响。
哪个属性值可能会在敌人的Animator组件中引起这种情况?
-
将
Update Mode设置为动画物理。 -
将
Culling Mode设置为完全剔除。 -
将
Culling Mode设置为始终动画。 -
将
Update Mode设置为未缩放时间。 -
你有一组番茄植物的游戏对象。番茄植物上的每个番茄都有一个名为
Tomato的脚本。
为了避免番茄植物重复出现,一些艺术家已经关闭了tomato游戏对象,这样它们就看不见了。
在场景开始时,我们需要计算场景中包括隐藏的番茄总数。
哪个命令可以获取所有Tomato脚本的引用?
-
GetComponentsInChildren(typeof(Tomato), true) -
GetComponentInChildren(typeof(Tomato), true) -
GetComponentsInChildren(typeof(Tomato)) -
GetComponenstInParent(typeof(Tomato), true) -
哪个静态
Time类属性会用来冻结时间?-
timeScale -
maximumDeltaTime -
captureFramerate -
time
-
-
在状态机中,以下哪个选项对标签最有用?
-
枚举
-
字符串
-
浮点数
-
整数
-
-
以下哪个与游戏中触发事件相关?
-
正在运行粒子效果。
-
玩家在菜单屏幕上闲置了 20 分钟。
-
玩家按下 UI 按钮。
-
玩家移动鼠标光标。
-
-
你创建了一个游戏,你的玩家必须潜行并避开敌人。在其中一个任务中,你的玩家必须注意仓库中的敌人(听脚步声、谈话声等)。
你会为这个游戏添加哪个音频属性?
-
为每个敌人添加音频源组件,将其空间混合设置为 3D,并播放声音。
-
当敌人靠近时,使用音频混频器快照添加低通滤波器。
-
测量每个敌人与玩家之间的距离,如果距离低于某个阈值,则播放声音。
-
添加一个背景音乐播放的音频源,并根据最近敌人的距离增加或减少音量。
-
在你的
CustomRolloff -
SpatialBlend -
ReverbZoneMix -
Spread
- 你已经开始为你的游戏添加音乐和音效。在测试时,你注意到当播放某些音效时,背景音乐会中断。
在音频源组件中哪个属性可以解决这个问题,使得你的音乐不会中断?
-
提高优先级。
-
增加音量。
-
增加 MinDistance。
-
降低 SpatialBlend。
-
你被要求制作一个 UI 菜单屏幕。你已经创建了一个画布,并将其渲染模式设置为屏幕空间 - 叠加。
在画布缩放器组件中,UI 缩放模式中的哪个属性可以使 UI 元素在屏幕大小变化时保持相同的像素大小?
-
恒定像素大小
-
与屏幕大小缩放
-
恒定物理大小
-
禁用画布缩放器
-
当在图像组件中勾选“保留纵横比”复选框时,这会做什么?
-
将相机的纵横比设置为与图像的透视相匹配。
-
使图像与相机的相同纵横比匹配。
-
图片保留了其原始尺寸。
-
对图像组件没有影响,仅对精灵渲染器有影响。
-
-
是否可以使用精灵渲染器代替画布中的图像组件?
-
不。尽管精灵渲染器可以在 2D/3D 空间中工作,但它不是用于画布的,因此不会工作。
-
是的,但精灵渲染器功能较少,是图像组件的较旧版本。
-
根据 Unity 项目,如果你的场景处于 2D 模式,是的。
-
是的,当用于动画精灵图集时。
-
-
Toggle按钮是开启还是关闭 -
当玩家在运行时按下它时,使图形激活或非激活。
-
持有勾选标记图像
parent游戏对象。
第十一章:第十一章:存储数据和音频混合器
在本章中,我们将探讨为我们的游戏存储和发送数据的常见方法。这还将涉及我们使用 Unity 的现成 音频混合器 来存储玩家的游戏音量设置。
如您所忆,在前一章中,我们从头开始制作自己的暂停屏幕。我们将在本章继续这一工作。我们仍然需要处理暂停屏幕上的音乐和音效滑块。我们将保留所有要在 音频混合器 中播放的音频源控制。音频混合器 将作为所有声音的中心点,也可以通过脚本进行操作,我们将在本章中这样做。如果我们的游戏有更多的音效和音乐,一个控制游戏声音的 音频混合器 从一个地方控制所有声音,将帮助我们避免与所有附加到游戏对象的不同的音频源组件纠缠在一起。
我们将使用 Unity 自带的 PlayerPrefs 来存储音量设置,它会在游戏运行的平台本地存储数据。这也被称为持久数据,因为我们可以在关闭包含音量信息的机器后,当机器重新开启时,数据仍然保留在系统中。我们将介绍将我们的对象序列化成数据,并将数据反序列化回对象。如果我们想将数据批量发送到数据库,这很有用。
在本章中,我们将涵盖以下主题:
-
使用 音频混合器
-
存储数据
让我们开始吧!
本章涵盖的核心考试技能
以下是在本章中将要涵盖的核心考试技能:
编程核心交互:
- 实现和配置游戏对象的行为和物理。
开发应用程序系统:
-
解析应用界面流程的脚本,例如菜单系统、UI 导航和应用设置。
-
分析用于用户进度功能的脚本,例如得分、等级和游戏内经济,利用 Unity Analytics 和
PlayerPrefs等技术。 -
识别用于保存和检索应用程序和用户数据的脚本。
为场景和环境设计编程:
- 确定实现音频资源的脚本。
在专业软件开发团队中工作:
- 识别构建模块化、可读性和可重用性脚本的技巧。
技术要求
本章的项目内容可以在 github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_11 找到。
您可以下载每个章节的项目文件的全部内容,地址为 github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition。
本章的所有内容都保存在章节的unitypackage文件中,包括一个包含本章我们将执行的所有工作的Complete文件夹。
查看以下视频以查看代码执行情况:bit.ly/3EYHpxf。
使用音频混音器
随着游戏的增长,有一个专注于所有分配的音量和音效的混音器通道是有用的。否则,如果没有单独的混音器通道,我们就需要在检查器窗口中点击各种游戏对象并调整它们的每个组件。
对于我们的游戏,我们将保持简单,创建三个没有任何附加效果的音频组。让我们看看每个音频组将关注什么:
-
主音频组:控制整个游戏的主音量
-
音乐音频组:控制每个级别的音乐
-
效果音频组:控制玩家飞船发射的子弹音效
以下截图显示了音频混音器和三个音频组的设置:
![Figure 11.1 – 带有主、音乐和效果音频组的音频混音器窗口
![img/Figure_11.01_B18381.jpg]
Figure 11.1 – 带有主、音乐和效果音频组的音频混音器窗口
更多信息
如果您想了解更多关于音频混音器布局的信息,请查看docs.unity3d.com/Manual/AudioMixerOverview.html上的文档。
让我们现在开始创建 Unity 编辑器中的音频混音器,按照以下步骤进行:
-
在
资产中右键单击文件夹内的空白区域。 -
从下拉菜单中选择创建,然后选择音频混音器。以下截图显示了选择过程:
![Figure 11.2 – 创建音频混音器
![img/Figure_11.02_B18381.jpg]
Figure 11.2 – 创建音频混音器
- 这样,就有一个新的
MasterMixer。
在我们将混音器连接到LevelMusic和Player_Bullet游戏对象(因为这些是制造声音的两个游戏对象)之前,我们需要进入音频混音器并首先创建音乐和效果混音器(我们目前只有单独的音频组)。
让我们更仔细地看看我们的MasterMixer,在项目窗口中双击MasterMixer文件。我们将看到以下屏幕:
![Figure 11.3 – 带有主音频组的音频混音器窗口
![img/Figure_11.03_B18381.jpg]
Figure 11.3 – 带有主音频组的音频混音器窗口
上一张截图显示了我们的音频混音器设置。它由四个类别组成:
-
MasterMixer。 -
快照:将此视为混音器的保存状态。我们可以有多个快照,例如物理 Hi-Fi,我们可以选择不同的已保存预设(摇滚、迪斯科、古典等)。快照的使用方式与预设相同;它节省了我们的时间,这样我们就不必总是调整混音器设置。
-
MasterMixer将是我们两个分组 – 一个用于音乐,另一个用于效果。我们很快就会创建这些。 -
视图:用于保存不同的音频混音器UI 布局。
不要过于担心细节,因为我们主要将关注分组。我们可以用音频混音器做更多的事情。查看官方 Unity 文档以获取更多信息,链接为docs.unity3d.com/Documentation/Manual/AudioMixer.html。
要在Master旁边添加两个额外的音量混音器,我们需要执行以下操作:
- 右键点击分组部分下的Master,然后选择添加子分组。我们将获得一个新的混音器,如下面的截图所示:

图 11.4 – 创建新的音频分组
-
右键点击上一个截图中的New Group(已圈出),然后从下拉菜单中选择重命名。
-
将
New Group重命名为Music。 -
重复步骤 1-3以创建另一个
Effects。
以下截图显示了现在带有所有三个 ASV 的音频混音器窗口将看起来像什么:

图 11.5 – 包含所有三个音频分组的音频混音器
太好了!现在,我们可以继续连接这些LevelMusic游戏对象,它是GameManager游戏对象的子对象。
要更新我们的LevelMusic游戏对象的音频源组件,请执行以下操作:
-
从 Unity 编辑器加载
bootUp场景。 -
在
GameManager游戏对象中,选择LevelMusic游戏对象。 -
在项目窗口中,点击MasterMixer左侧的箭头以展开其内容。
-
点击Music子分组,并将其拖入 LevelMusic 的音频源的输出字段,如下面的截图所示:

图 11.6 – 将音乐音频分组拖到 LevelMusic 音频源输出字段
- 在检查器窗口中点击覆盖|应用全部以更新 GameManager 的预制件设置。
现在,我们需要为我们的player_bullet预制件做类似的事情。要使用Effects混音器更新其音频源,请执行以下操作:
-
在
Assets/Prefab/Player文件夹中。在那里,你应该找到我们的player_bullet游戏对象。 -
选择
player_bullet(表示为player_bullet的音频源****输出(表示为3)。
以下截图显示了player_bullet游戏对象的音频源在检查器窗口中应该看起来像什么:

图 11.7 – 将 player_bullet 的音频分组效果拖入音频源输出字段
现在,音频混音器几乎准备好连接到暂停屏幕的音乐和效果滑块。在我们进入下一节之前,我们需要做最后一件事,那就是使音频组的音量可访问,以便我们可以使其与暂停屏幕的音频滑块进行通信。为此,我们需要将我们的音频混音器的衰减音量属性设置为打开或暴露给我们的脚本。
要暴露并命名我们的音频组,我们需要执行以下操作:
-
在项目窗口中,转到MasterMixer并展开其内容,以便我们可以看到其组。
-
选择音乐组。
-
在检查器窗口中,我们看到了音乐****组的属性。我们想要暴露音量,以便我们可以用我们的脚本修改它。
-
右键点击音量(在检查器窗口中,位于衰减下)并选择将“音量(音乐)”暴露给脚本,如图下所示:
![Figure 11.8 – 在检查器窗口中,右键点击衰减音量并选择将“音量(音乐)”暴露给脚本'
![img/Figure_11.08_B18381.jpg]
Figure 11.8 – 在检查器窗口中,右键点击衰减音量并选择将“音量(音乐)”暴露给脚本'
现在,我们也有选择给暴露的音量一个引用名称,而不是其默认名称MyExposedParam。
要更改暴露的音乐****音量的引用,请执行以下操作:
-
返回到
MasterMixer文件。 -
如您可能已经注意到的,在音频混音器的右上角,我们被告知我们有暴露参数(1)(在下述截图中的1表示)。这个1是我们刚刚暴露的音乐****音量。
-
点击暴露参数(1)(标记为1)。
-
右键点击我的暴露参数音量(音乐)(标记为2)。
-
从下拉菜单中选择重命名(标记为3)。
-
在出现的参数中,将其重命名为
musicVol(标记为4)。
以下截图显示了我们在前面的步骤中提到的阶段:
![Figure 11.9 – 将 MyExposedParam 重命名为 musicVol
![img/Figure_11.09_B18381.jpg]
Figure 11.9 – 将 MyExposedParam 重命名为 musicVol
我希望你们已经很好地理解了这个过程,因为我希望你们再次执行这个过程,但这次是使用 effectsVol。
最后,在我们的音频混音器中,我们将暴露的音量引用命名为如下:
![Figure 11.10 – 两个暴露的参数都已重命名
![img/Figure_11.10_B18381.png]
Figure 11.10 – 两个暴露的参数都已重命名
很好!在我们进入下一节之前,让我们简要回顾一下本节我们已经涵盖的内容:
-
我们介绍了音频混音器及其优点。
-
我们为我们的混音器创建了音频组。
-
我们将音频混音器附加到了游戏对象的音频源上。
-
我们将音频混音器暴露给了我们的脚本。
在下一节中,我们将编写暂停屏幕的音量和效果滑块的代码。
将音频混音器附加到 UI 滑块
在本节中,我们将编写两个方法(SetMusicLevelFromSlider和SetEffectsLevelFromSlider),将我们的暂停屏幕的音乐和效果滑块连接到我们在上一节中创建的音频混音器。
让我们先通过脚本将音乐滑块添加到我们的音频混音器中,如下所示:
- 在
PauseComponent脚本中,它应位于Assets/Script,并打开它。
因为我们将要访问音频混音器,我们需要一个额外的 Unity 库来实现这一点。
-
在我们的
PauseComponent脚本顶部,添加以下代码行:using UnityEngine.Audio; -
现在,添加一个字段来存储我们的音频混音器的引用:
[SerializeField] AudioMixer masterMixer; -
我们还需要为
masterMixer变量添加两个更多变量,如下所示:[SerializeField] GameObject musicSlider; [SerializeField] GameObject effectsSlider; -
保存脚本并返回到 Unity 编辑器。
-
加载我们开始创建暂停屏幕的
level1场景。 -
从层次结构窗口中选择
PauseContainer游戏对象。 -
在
PauseComponent。在这里,我们可以从层次结构窗口拖动两个滑块,以及从项目窗口中的MasterMixer,如下截图所示:
![图 11.11 – 将每个游戏对象和文件拖入其指定的字段]
图 11.11 – 将每个游戏对象和文件拖入其指定的字段
现在我们已经将三个引用(音乐滑块、效果滑块和主混音器)连接到它们的参数,我们可以返回到我们的PauseComponent脚本并为暂停屏幕的每个音量滑块编写一个方法。
为了添加功能,使我们的音乐滑块控制音乐混音器,请执行以下操作:
-
在
PauseComponent脚本中,在PauseComponent类内添加一个public方法:public void SetMusicLevelFromSlider() { masterMixer.SetFloat("musicVol",musicSlider.GetComponent<Slider> ().value); }
我们刚刚进入的public方法SetMusicLevelFromSlider将作为masterMixer的事件。在这个变量中,我们调用它的SetFloat函数,该函数接受两个参数。第一个是混音器的引用名称(我们在本章中将其称为musicVol),而第二个是要更改的值。我们正在发送来自暂停屏幕的音乐滑块的值。
- 保存脚本并返回到 Unity 编辑器。
接下来,我们需要附加我们的SetMusicLevelFromSlider方法。为了使音乐滑块与该方法通信,请按照以下步骤操作:
-
仍然在我们的
level1场景中,在Music游戏对象中。 -
在检查器窗口中,点击检查器窗口底部的+按钮,以便将事件附加到滑块组件(以下截图中的1所示)。
-
将
PauseContainer游戏对象从层次结构窗口拖到无(对象)参数(以下截图中的2所示)。 -
点击
SetMusicLevelFromSlider(以下截图中的3所示)。
以下截图引用了检查器窗口中Music游戏对象的先前说明:
![图 11.12 – 更新音乐游戏对象的音乐滑块事件的三个步骤]

图 11.12 – 更新音乐游戏对象音乐滑块事件的三个步骤
如果我们回到启动场景,在 Unity 编辑器中点击播放,然后在游戏窗口中,当游戏暂停按钮出现时点击它,我们将能够通过音乐滑块调节音量大小。
现在,我们需要以类似的方式重复此过程,以便我们的效果滑块音量能够工作:
-
返回到
PauseComponent脚本,并输入以下方法:public void SetEffectsLevelFromSlider() { masterMixer.SetFloat("effectsVol",effectsSlider. GetComponent <Slider>().value); }
如我们所见,代码几乎与SetMusicLevelFromSlider的代码相同,除了变量名不同。
-
保存脚本。
-
返回到 Unity 编辑器,重复相同的步骤拖动
PauseComponent游戏对象,但这次是使用Effects游戏对象,并选择SetEffectsLevelFromSlider,如下面的截图所示:


图 11.13 – 更新效果游戏对象音频滑块
最后,测试一下当我们运行游戏时,效果滑块是否工作。
这显然只能在level1中工作,因为level2和level3没有额外的游戏对象。在下一章中,我们将创建一个新的level3,所以如果你能等到那时,就可以避免再次经历删除和添加场景的过程。
在本节中,我们介绍了PauseComponent脚本的以下功能:
-
确保它识别音频混音器
-
确保音乐和效果UI 滑块改变音频组
在下一节中,我们将开始探讨如何存储我们的数据。我们将再次使用暂停屏幕来展示游戏记住我们的音量设置的好处。
存储数据
在本节中,我们将介绍如何存储我们的数据,例如游戏的音量设置,这样当我们玩游戏时,我们就不必不断将音量设置回之前的位置。我们希望游戏能为我们记住这些设置。
存储数据有多种方式。我们将介绍的是 Unity 开发中最常见的两种选择。它们如下:
-
使用
PlayerPrefs来保存我们的音量设置,这样当我们关闭并重新打开游戏时,它会记住我们的设置。PlayerPrefs可以通过文本文件读取器轻松地从游戏外部访问。在开发过程中,确保不要使用PlayerPrefs来存储敏感信息,例如信用卡详情或会给玩家不公平优势的信息,如存储生命值、能量、得分、游戏内信用等。有关PlayerPrefs的更多信息,请查看 Unity 的描述docs.unity3d.com/Documentation/ScriptReference/PlayerPrefs.html。 -
PlayerPrefs的特点是它使用类似于PlayerPrefs的数据类型,如int、float和string,但还包括object(我们的类作为蓝图,以便我们可以创建对象)、array、bool和null。
使用这种形式的 应用程序编程接口(API)来传输游戏数据(生命值、关卡、玩家进度、能量等)是明智的,但不要将有关游戏内信用、银行详情、个人地址和电子邮件的高度个人详细信息本地存储,除非你正在使用某种形式的加密。
信息
一个 API 基本上告诉我们应用程序之间是如何相互通信的。
关于 Unity 中的 JSON 的更多信息,请查看docs.unity3d.com/Documentation/Manual/JSONSerialization.html 的文档。
在以下章节中,我们将基于它们是官方支持的,并且可能会在考试中提到的两种方式来介绍如何在 Unity 中存储数据:
-
PlayerPrefs和音量设置 -
JSON 和存储游戏统计数据
-
添加 JSON 变量
让我们从如何使用 PlayerPrefs 开始,并再次回顾我们的暂停屏幕。
PlayerPrefs 和音量设置
如我们所知,我们的游戏在暂停屏幕上有音乐和音效的音量控制。为了使我们的游戏记住这些音量设置,即使游戏已经关闭并重新打开,我们需要做以下操作:
-
在
Assets/Script。 -
双击
PauseComponent脚本。 -
滚动到
SetMusicLevelFromSlider方法,并在方法范围内但底部添加以下额外的代码行。以下代码显示了添加代码后方法现在的样子:public void SetMusicLevelFromSlider() { masterMixer.SetFloat("musicVol",musicSlider. GetComponent <Slider>().value); PlayerPrefs.SetFloat("musicVolume",musicSlider. GetComponent <Slider>().value); // << NEW CODE LINE }
在上述代码中,我们使用了音乐 <Slider> 组件的 value,并将其 float value 应用到 PlayerPrefs 的 float 上,其中 musicVolume 作为我们的键(用于识别 PlayerPrefs 值的参考名称)。
-
对
effects方法也做同样的操作:public void SetEffectsLevelFromSlider() { masterMixer.SetFloat("effectsVol",effectsSlider. GetComponent <Slider>().value); PlayerPrefs.SetFloat("effectsVolume",effectsSlider. GetComponent <Slider>().value); // << NEW CODE LINE }
这样我们的 PlayerPrefs 文件就准备好了,可以存储音乐和音效音量。接下来要做的是在下次从 PlayerPrefs 加载关卡时重新应用音乐/音效音量。
要从我们的 PlayerPrefs 中获取音乐音量设置,请执行以下操作:
-
再次打开
PauseComponent脚本。 -
在
PauseComponent类中,在Awake函数的底部输入以下代码:masterMixer.SetFloat("musicVol",PlayerPrefs.GetFloat("musicVolume")); masterMixer.SetFloat("effectsVol",PlayerPrefs.GetFloat("effectsVolume"));
在上述代码中,我们正在重新应用我们保存的 PlayerPrefs 值(音乐和音效音量,都是浮点数)到音频混音器的 音频组。
我们想要混音器拥有的音量现在已设置。我们最后需要做的是将两个音量滑块设置到它们的 UI 位置。
-
要更新
PauseComponent:float GetMusicLevelFromMixer() { float musicMixersValue; bool result = masterMixer.GetFloat("musicVol", out musicMixersValue); if (result) { return musicMixersValue; } else { return 0; } }
上述代码是一个返回名为 GetMusicLevelFromMixer 的 float 值的方法。
让我们来看一下 GetMusicLevelFromMixer 的步骤:
-
在这个方法中,我们创建了一个名为
musicMixersValue的float变量。 -
在
musicMixersValue之后的行检查masterMixer实例是否包含musicVol。我们知道它包含,因为我们之前在将每个音量设置从 音频混音器 公开时设置了它,如下面的截图所示:

图 11.14 – 我们命名的公开参数
-
因此,如果
masterMixer包含一个名为(键)musicVol的float值,我们将将其存储在名为musicMixersValue的float中。 -
如果
masterMixer包含一个在bool值中存储的float,则masterMixer.GetFloat将发送一个true或false值;否则,它将返回0。 -
如果
bool值为true,则方法会返回masterMixer中的float值;否则,它将返回0。
接下来,我们需要调用这个 GetMusicLevelFromMixer 并使其将值发送到音乐滑块。现在让我们编写这段代码。
-
在
PauseComponent脚本中,在顶部,在Awake函数中,在两个masterMixer编码行下面添加以下代码:musicSlider.GetComponent<Slider>().value = GetMusicLevelFromMixer();
在前面的代码片段中,我们在关卡开始时将 GetMusicLevelFromMixer 的结果发送到 musicSlider 的值。
这就是我们的音乐滑块设置。现在,我们需要为我们的效果滑块重复此过程。过程是相同的,只是使用效果滑块的变量,所以不重复相同的步骤,我想让你做以下操作:
-
使用与
GetMusicLevelFromMixer相同的代码模式创建一个GetEffectsLevelFromMixer方法,但使用effectsVol而不是musicVol。 -
在
Awake函数中将GetEffectsLevelFromMixer的结果分配给effectsSlider变量。使用musicSlider变量作为参考。
尝试一下 – 如果你遇到困难,请查看本书 GitHub 存储库中的 Complete 文件夹。
保存脚本并返回到 Unity 编辑器。播放第一关,调整音量,退出游戏,然后返回到第一关,查看我们的音量是否已保存到音乐和效果滑块。
现在,我们将继续学习如何以稍微不同的方式存储和发送数据。
JSON 和存储游戏统计数据
JSON 非常适合在我们的游戏中创建、存储和更新信息。正如我们在本章前面提到的,JSON 通常用于将游戏中的数据发送到在线服务器,JSON 数据可以传递到另一组数据。
我对 JSON 的最佳解释是一个类比,我就像在一家餐厅,坐在餐桌旁(我的游戏);服务员过来,接收我的(JSON)订单,然后将其发送到厨房(在线服务器)。最后,服务员带着我的食物回来。
关于 JSON 编码,我们是在一个单独的类中存储变量,然后序列化这个类(对象)为数据(系统内存或文件)。从那里,我们可以将此数据传输到实际的文件或上传到数据库服务器。整个过程也可以反过来,即我们取数据并将其返回为对象。这被称为反序列化。
现在,让我们继续编写一些 JSON 值。
添加 JSON 变量
使用 JSON 的目的是创建一种简单的方式来存储和更新数据。在我们的项目中,我们将提供一个存储游戏统计数据的简单示例。当玩家完成游戏时,我们将存储数据并将其放入 JSON 格式。
我们将要存储的三个变量如下:
-
livesLeft:玩家剩余的生命值 -
completed:当玩家完成游戏时 -
score:存储玩家的得分
让我们从创建一个新的脚本开始,该脚本将接收我们游戏的三个统计更新。然后,这些更新将被转换为 JSON 格式。按照以下步骤操作:
-
创建一个新的脚本(如果你不知道如何操作,请回顾第二章**,添加和操作对象)中的通过脚本更新我们的相机属性*部分。
-
将新脚本命名为
GameStats。 -
在我们打开
GameStats脚本之前,我建议你将文件存储在Assets/Script文件夹位置。 -
接下来,我们可以打开
GameStats脚本并编写以下变量:public class GameStats { public int livesLeft; public string completed; public int score; }
注意到GameStats脚本不需要库或需要继承MonoBehaviour。我们不需要这些额外的功能。
当玩家完成游戏时,我们将这三个读数以 JSON 格式存储。从那里,我们可以将此数据转换为 JSON 文件。这个过程被称为序列化。
序列化/反序列化
这两个术语基本上指的是数据存储的方向。
序列化:这指的是将我们的脚本中的对象转换为字节(在我们的例子中是一个文件)。
反序列化:正如你可能想象的那样,反序列化是序列化的相反。这意味着我们正在将我们的原始数据(文件)转换为对象。
- 保存脚本。
接下来,我们需要编写一些代码来更新玩家的生命值、时间和日期以及得分。我们将在玩游戏并完成第 3 级时这样做。在这种情况下,我们需要前往我们的ScenesManager并更新代码。
为了更新我们的ScenesManager,使其能够读取玩家的统计数据并将它们转换为 JSON 格式,我们需要做以下操作:
-
在 Unity 编辑器中,前往我们的
ScenesManager脚本所在的位置。这应该在Assets/Script文件夹中。 -
双击文件以在 IDE 中打开它,并滚动到检查游戏是否结束的位置。这位于
GameTimer方法中。
以下截图显示了在 ScenesManager 脚本中需要添加我们的新方法的位置:

图 11.15 – 在我们的 ScenesManager 脚本中标记了添加新代码的位置
在前面的截图中,有一个星号(string 参数,它将是已完成的关卡名称)。
-
在前面的截图中的 ***** 处输入以下方法名称:
SendInJsonFormat(SceneManager.GetActiveScene().name); -
接下来,我们需要创建
SendInJsonFormat方法。在ScenesManager脚本中向下滚动到一个点,我们仍然在其类内部,但不在另一个方法中,并输入以下内容:void SendInJsonFormat(string lastLevel) { if (lastLevel == "level3") { GameStats gameStats = new GameStats(); gameStats.livesLeft = GameManager.playerLives; gameStats.completed = System.DateTime.Now.ToString(); gameStats.score = GetComponent<ScoreManager>(). PlayersScore; string json = JsonUtility.ToJson(gameStats,true); Debug.Log(json); } }
在前面的代码中,我们执行以下一系列步骤:
-
我们有一个
SendInJsonFormat方法,它接受一个string参数。 -
在
SendInJsonFormat方法内部,我们设置了一个if语句,检查lastLevel字符串是否包含level3值。 -
如果
lastLevel等于level3字符串,我们将在if语句内部执行以下步骤: -
我们创建了一个
GameStats类的实例,该类是在本章早期创建的。 -
我们访问其
livesLeftpublic变量,并应用来自GameManager类的静态playerLives变量。 -
下一个变量显示了我们完成游戏的日期和时间。我们从
System库中发送命令,该库提供了日期和时间,并将其转换为string(ToString())。我们将此结果发送到gameStats实例的completed变量中。 -
我们发送数据的最后一个变量是玩家的得分。我们从
ScoreManager类的playerScorestatic变量中获取它。
现在我们已经将三个变量应用到我们的 gameStats 实例上,我们可以使用 Unity 的 JsonUtility 类将我们的 gameStats 发送到 ToJson 函数。
我们也可以通过将 true 添加到参数中来使 JSON 数据可读,这样当我们向控制台发送 log 命令以查看此操作是否正确执行时,我们可以读取结果。
- 保存脚本,然后回到 Unity 编辑器,并从
bootUp场景完成游戏到gameOver场景。
以下截图显示了我在玩游戏并完成第 3 级时控制台的日志输出:

图 11.16 – 显示我们游戏统计信息的控制台窗口
如您所见,我们有来自我们脚本的但以 JSON 格式显示的数据。
此信息可以保存到物理文件,或者可以发送到服务器以记录我们的玩家游戏情况,并在以后的项目中反序列化结果(如果您对此感兴趣,请查看以下提示)。关键是我们在存储和携带数据,这些数据可以被发送出去,供我们或其他系统拾取、存储和修改。
更多信息
到目前为止,我们已经成功从我们的对象中提取变量,并将它们转换为 JSON 数据格式(序列化)。
现在,假设我们更改了我们的数据(更改其值),并希望将那些数据带回到我们的游戏代码和类中。反向方法将是 GameStats loadJsonData = JsonUtility.FromJson<GameStats>(json);。
这将更新我们的 GameStats 变量,从 JSON 文件中。您可以想象这将方便在游戏中保存和加载数据。
接下来,我们将最新的 JSON 数据文件发送到设备(我们玩游戏的那台机器)。为了创建和存储包含我们自定义统计数据的 JSON 文件,请执行以下操作:
-
返回到
ScenesManager脚本。 -
滚动到我们创建的
SendInJsonFormat方法处。 -
在方法的
if语句的底部,在if语句的作用域内,添加以下两行代码:Debug.Log(Application.persistentDataPath + "/GameStatsSaved.json"); System.IO.File.WriteAllText(Application.persistentDataPath + "/GameStatsSaved.json",json);
上述代码块显示我们不一定需要添加 Debug.Log,并显示了下一行代码创建和存储我们的 JSON 文件的位置。每个平台都会在不同的文件夹中存储数据。有关不同平台的位置信息,请参阅 Unity 关于持久数据的官方文档 docs.unity3d.com/ScriptReference/Application-persistentDataPath.html。
我的系统是 Windows PC,所以 Debug.Log 将在我的系统上显示以下位置:

图 11.17 – JSON 文件的 Windows 位置
我们刚刚输入的代码的第二行正在使用系统库,并使用一个函数 (Application.persistentDataPath),该函数将引用我们的设备本地存储。然后,在函数之后,我们添加了我们想要用来引用我们的 JSON 文件的名字 (/GameStatsSaved.json),以及格式类型,即 json。
-
保存脚本。
-
返回 Unity 编辑器,玩到游戏结束,并前往控制台屏幕上显示的位置。以下截图显示了我们的游戏创建的文件位置:

图 11.18 – 我们 JSON 文件的 Windows 位置
- 双击文件以查看其内容。正如您将看到的,这是我们游戏统计数据存储的地方,如下面的截图所示:

图 11.19 – 我们 JSON 文件的内容
通过这种方式,我们现在知道了如何存储非敏感数据,例如我们的游戏音量 (PlayerPrefs),以及如何创建、存储和发送其他类型的 JSON 格式数据。
现在,让我们总结本章内容。
更多信息
更多关于所有这些事件的信息,请查看官方 Unity 文档 docs.unity3d.com/Manual/UnityAnalyticsEvents.html。
概述
本章涵盖了各种主题,包括理解 Unity 的音频混音器,这是我们控制游戏中的声音的地方,以及使用我们的脚本调整级别。然后,我们继续前进,探讨了使用PlayerPrefs和 JSON 格式的自定义存储来存储数据,以便识别两种存储数据方式之间的差异。对于 JSON,我们将基于对象的数据转换为字节,并将结果存储在文件中(序列化)。
在未来的项目中,你可能会用到我们在前两章中介绍的编码,比如存储和重新应用数据,例如音乐和音效音量滑块。希望你能通过在项目中使用其他组件,进一步利用这些数据,使你的游戏能够将数据发送到云端,并监控玩家的进度,作为有助于改进开发的反馈。
在下一章中,我们将探讨路径查找以及如何提高我们游戏的整体性能。
第十二章:第十二章:NavMesh、时间轴和模拟测试
在本章中,我们将介绍 Unity 为开发者提供的两个主要功能,用于向游戏对象分配AI以及支持逻辑的动画。
Unity 为我们游戏对象提供了一个现成的系统,可以发出路径查找算法,其中游戏对象可以被赋予一个巡逻区域。这在一系列使用敌方士兵在走廊上来回寻找玩家的游戏中非常有用。士兵可以根据赋予他们的行为做出反应。
我们将要介绍的另一个功能是时间轴组件,它用于游戏/电影中的场景如电影场景的动画。你可能认为我们在第四章,“应用艺术、动画和粒子”中已经介绍了动画系统。是的,你是对的,但对于一个包含多个游戏对象和动画、过渡和状态可能很快变得复杂的复杂动画,时间轴支持一系列特定于我们代码的轨道,并且我们可以向时间轴添加我们自己的自定义动画轨道。
这两个主要功能将被分配给我们的 Killer Wave 游戏项目。导航网格(NavMesh)控制着一群小型非玩家角色(NPC)机器人,它们会远离玩家的飞船,就像它们在恐慌中为了生存而移动一样。
时间轴将用于应用一个中等难度的电影场景,玩家将看到关卡结束时的 Boss 冲过他们,场景中的灯光会闪烁红色。
最后,我们将以最后一个迷你模拟测试结束本章,该测试将包括涵盖本章和之前章节内容的问题。
本章将涵盖以下主题:
-
准备最终场景
-
使用 NavMesh 开发 AI
-
探索时间轴
-
延长时间线
让我们先回顾一下本章涵盖的技能。
本章涵盖的核心考试技巧
编程核心交互:
-
实现游戏对象和环境的行为和交互
-
确定实现摄像机视图和移动的方法
在艺术管道中工作:
-
了解材质、纹理和着色器:Unity 渲染 API
-
了解照明:Unity 照明 API
-
了解二维和三维动画:Unity 动画 API
场景和环境设计编程:
- 使用 Unity 导航系统确定路径查找脚本
技术要求
本章的项目内容可在github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition找到。
您可以下载每个章节的项目文件的完整内容,链接为 github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition。
本章的所有内容都保存在章节的 unitypackage 文件中,包括一个包含我们在本章中将要完成的所有工作的 Complete 文件夹。
查看以下视频以查看 代码执行情况:bit.ly/3LsC0B4。
准备最终场景
在本节中,我们将分两部分准备我们的新 level3 场景 – 游戏的第一部分将有一些三维艺术资源供玩家可能与之碰撞。此外,环境还将用于我们的新逃跑敌人。本节的第二部分用于升级摄像机的行为,使其不再是静态的,我们现在需要它穿越关卡。玩家也将获得与摄像机相同的固定速度。
到本节结束时,我们将设置好环境,如下面的截图所示:

图 12.1 – 我们的第 3 级资源
上一张截图从左到右的箭头显示了摄像机的路径。一旦它到达一个特定的点,它就会停止,然后不久关卡就会结束。
值得注意的是,在 level3Env 预制件中有一个名为 BossTrigger 的脚本的游戏对象。此游戏对象包含一个盒子碰撞体、一个刚体和一个 BossTriggerBox 脚本。
BossTrigger 游戏对象的目的激活 BossTrigger 碰撞体。我们将在本章的 探索时间轴 部分进一步讨论这一点。
让我们继续,将我们的 level3 文件的环境移动到场景中:
-
从
Assets/Scene中双击level3场景文件。 -
从
Assets/Prefab。 -
将
level3Env预制件拖放到 Hierarchy 窗口中的_SceneAssets游戏对象。 -
从
2500、17和-24中选择level3Env -
0.55、0.55和0.55
以下截图显示了我们的场景设置,已经准备好让玩家飞入:

图 12.2 – 第 3 级资源已放置
因此,我们需要对我们的 level3 场景做一些修改,以确保摄像机支持新的环境,并且环境本身没有我们不需要的额外资源,例如背景的动画纹理四边形和飞过星星的预制粒子系统。请注意,如果这些游戏对象处于预制状态,请确保解包(在 Hierarchy 中右键单击游戏对象 Prefab | Unpack),否则我们将更改 1 和 2 级别的内 容。
通过以下方式在 Hierarchy 窗口中修改 level3 场景:
-
幸运的是,上述两个资源都位于
GameSpeed游戏对象中的GameSpeed游戏对象内。 -
在我们的场景中,我们不会使用任何方向性光照,因此也从层次结构窗口中移除
Directional Light。 -
此外,为了在没有经过整个游戏循环的情况下运行我们的
level3场景,我们可以将GameManager预制件从Assets/Prefab文件夹位置拖放到层次结构窗口中。
现在,我们的level3将看起来像以下截图所示:
![图 12.3 – 到目前为止的层次结构布局
![图 12.03_B18381.jpg]
图 12.3 – 到目前为止的层次结构布局
现在,我们需要让Camera组件支持远裁剪平面值。为此,我们需要执行以下操作:
-
在层次结构窗口中选择主摄像机。
-
在
1300。
因此,我们已经调整了摄像机的裁剪平面以显示level3文件的环境,移除了帮助我们之前级别的艺术资源的GameSpeed游戏对象,并将GameManager添加到level3以简化开发。现在,我们需要将注意力转向使摄像机真正移动,而不是像level1和level2那样创建移动的错觉。
我已经创建了一个小脚本,它将使摄像机从一个点移动到另一个点;脚本中的所有内容都展示了我们在本书中已经覆盖过的代码元素。因此,您不需要创建脚本,但理解它是显然的主要目的。
我们将把脚本附加到场景中的主摄像机上以控制其移动。按照以下说明操作:
-
从层次结构窗口中选择主摄像机。
-
点击
CameraMovement直到在下拉菜单中看到脚本出现。 -
选择CameraMovement脚本。
-
在下拉列表中点击
box collider。当它出现时,选择它并检查其Is Trigger框。 -
简而言之,此脚本将在激活后 6 秒沿x轴进行平移。当脚本到达特定点时,它将停止;它还将确保玩家不会随它一起移动。
让我们修改我们的Player脚本以对level3的摄像机移动做出反应:
-
在
Assets/Script中双击Player脚本以打开它。 -
在
Player脚本内部,在我们全局变量顶部的地方,输入私有变量及其属性:float camTravelSpeed; public float CamTravelSpeed { get {return camTravelSpeed;} set {camTravelSpeed = value;} } float movingScreen;
我们刚才输入的camTravelSpeed变量将用作额外的乘数,以设置摄像机沿x轴移动时的玩家飞船的节奏。
第二个变量movingScreen将保存Time.deltaTime乘以camTravelspeed的结果。稍后,movingScreen将用于比较玩家飞船的x轴。
-
在
Start函数中,在其函数底部添加以下行:movingScreen = width;
在 Start 函数内部,我们将 width float 测量值添加到 movingScreen 中(这发生在 width 在 Start 函数中更新之后),因为这将是它接收来自 Time.deltaTime 和 camTravelspeed 的增量之前的起始位置。
仍然在 Player 脚本中,滚动到 Movement 方法。
-
在
Movement方法的顶部,输入以下代码,这将乘以我们的玩家飞船的速度:if(camTravelSpeed > 1) { transform.position += Vector3.right * Time.deltaTime * camTravelSpeed; movingScreen += Time.deltaTime * camTravelSpeed; }
在我们刚刚输入的代码中,我们将运行一个检查来查看 camTravelSpeed 是否已从我们的新 CameraMovement 脚本中更新。如果 camTravelSpeed 已更新,我们将进入 if 语句的作用域。
在 if 语句内,我们将玩家的飞船的 x 轴向右增加,乘以 Time.deltaTime 和 camTravelSpeed。
我们做的第二件事是添加 movingScreen 值,它最初持有我们游戏区域的当前 width。然而,因为屏幕在移动,我们需要增加游戏区域,这样玩家就不会落后或超出摄像机的视野太远。
我们添加到我们的 Player 脚本中的最后一个修改是水平移动,仍然在 Movement 方法中。
-
滚动直到你到达玩家可以按 右箭头 键来移动的位置(
Input.GetAxisRaw("Horizontal") > 0)。在if语句的作用域内,我们可以修改第二个if语句为以下内容:if (transform.localPosition.x < movingScreen+(width/0.9f))
如果玩家在键盘/手柄上按下 右箭头 键,我们可以运行一个检查来查看玩家的 x 轴是否小于 movingScreen 浮点值;此外,我们还将包括一个缓冲区,将玩家推到屏幕边缘。
-
然后,我们可以在玩家在键盘/摇杆上按下 左箭头 键时,在第二个
if语句中做同样的事情:if (transform.localPosition.x > movingScreen+width/6)
在使用 movingScreen 变量时,适用类似的规则,该变量会随着轻微的缓冲区不断递增,以保持我们的玩家飞船在游戏屏幕内。
- 保存
Player脚本。
在我们继续到下一个脚本之前,我们需要在我们的新 CameraMovement 脚本中取消注释两行代码,以便它可以与 Player 脚本交互。
回到 CameraMovement 脚本,通过移除 // 来取消注释以下两行。需要取消注释的第一行是以下内容:
// GameObject.Find("Player").GetComponent<Player>().CamTravelSpeed = camSpeed;
需要取消注释的第二行是以下内容:
// GameObject.Find("PlayerSpawner").GetComponentInChildren<Player>().CamTravelSpeed = 0;
现在,这两行代码可以改变玩家飞船的速度。
接下来,我们需要更新我们的 GameManager 脚本,使其能够识别 level1、level2 和 level3 之间的差异,这些级别有移动的摄像机。
因此,让我们转到 GameManager 脚本,并添加两个主要元素——摄像机速度和识别场景之间的差异。让我们首先打开 GameManager 脚本:
-
从
Assets/Script。 -
双击
GameManager脚本来在您的 IDE 中打开它。
你可能已经或还没有查看我们附加到主相机的 CameraMovement 脚本,但在这个脚本内部有一个名为 camSpeed 的变量。这个变量操纵相机的速度;在我们的 GameManager 脚本中,我们设置了主相机的速度。
从这个例子中我们可以得出的主要结论是,CameraMovement 脚本将根据 GameManager 脚本中设置的值来操纵相机的速度。
-
在
GameManger脚本中,向下滚动到标题为CameraSetup的方法。 -
我们将使这个方法接受一个变量来改变相机的速度。将
CameraSetup方法更改为接受参数中的float值。CameraSetup方法将首先看起来如下:public void CameraSetup()
然后它将变成这样:
public void CameraSetup(float camSpeed)
-
在
CameraSetup方法中,我们需要将camSpeed传递到新的CameraMovement脚本中:gameCamera.GetComponent<CameraMovement>().CamSpeed = camSpeed;
注意,我们添加到 CameraSetup 方法的代码需要在将主相机存储在 gameCamera 变量之后添加。在继续之前,我们还需要从 PlayerSpawner 脚本中移除对 CameraSetup 的引用,因为我们也不再需要它了。
-
打开
PlayerSpawner脚本。 -
向下滚动并删除:
GameManager.Instance.CameraSetup()。 -
保存脚本并返回到
GameManager脚本。
在 GameManager 脚本中要做的最后一件事是更新 LightandCameraSetup 方法,以便当调用其中的 CameraSetup 方法时,它将接受一个设置主相机速度的值。因此,在 level1 和 level2 中,我们希望相机继续不移动;在 level3 中,我们需要给相机应用速度。
-
在
LightandCameraSetup中,用以下内容替换其原始的switch语句:switch (sceneNumber) { case 3 : case 4 : { LightSetup(); CameraSetup(0); break; } case 5: { CameraSetup(150); break; } }
因此,之前我们的 switch 语句在情况 3、4 和 5 中都运行了 LightSetup 和 CameraSetup。但现在,在之前的代码中,我们已经将角色分开。在情况 3 和 4 中,我们像往常一样运行 LightSetup,而现在,因为 CameraSetup 现在接受一个 float 值,我们将相机速度设置为 0。
在 5 情况下,这是我们的 level3 场景的构建号,我们忽略 LightSetup,因为我们在这个场景中不会使用方向光。我们运行 CameraSetup,但给它一个值为 150 的值,这也会是我们在方法内设置的 camSpeed 变量的值。保存 GameManager 脚本。
- 按下
level3场景开始播放。以下截图显示了到目前为止我们所拥有的内容:


Figure 12.4 – Level 3 in Play mode
上一张截图显示了在 Unity 编辑器中按下 Play 后发生的一系列事件。让我们按顺序逐一分析这些事件:
-
按下 Play 之前的
level3场景(标记为 1)。 -
场景处于播放模式,并设置了相机位置和背景(标记为 2)。
-
级别的 UI 是动画的,敌人开始飘入游戏窗口(标记为 3)。
-
玩家进入关卡,场景暂停几秒钟,然后玩家才能控制他们的飞船(标记为4.)。
-
摄像机开始跟随雷达摄像机移动,跟踪玩家的进度和即将到来的敌人(标记为5.)。
在下一节中,我们将添加我们的第二种敌人类型(逃跑敌人),它将在我们新的艺术作品中逃跑。
使用 NavMesh 开发 AI
在本节中,我们将介绍一个新敌人,该敌人将尝试从玩家的飞船上以狂热型行为逃跑。这个敌人的行为将通过使用 Unity 内置的NavMesh功能来实现。
如您所想象,Unity 的这个内置功能可以解决许多关于 NPC 游戏的问题,类似于《Metal Gear Solid》游戏中的问题,其中玩家必须偷偷摸摸地行动,以免被敌人士兵发现。
NavMesh为敌人士兵提供了一条行走路径,然后如果他们看到玩家,他们的行为将从巡逻变为攻击。
因此,在我们的游戏中,我们将实现NavMesh,但使其敌人的反应与他们在Metal Gear Solid中的反应不同。我们将向第三级场景添加多个成群的逃跑敌人。这种混乱、分散注意力的行为将使最终关卡对玩家更具挑战性。
以下截图显示了围绕我们的逃跑敌人有一个圆柱形半径。这个半径被称为代理半径,可以调整以阻止其他障碍物和敌人相互交叉:

图 12.5 – 逃跑敌人
在我们将这些逃跑敌人添加到场景之前,我们需要通过烘焙一个 NavMesh 来告诉逃跑敌人他们可以移动的地方。
首先,我们需要选择我们将用于烘焙的游戏对象,这也意味着我们需要取消选择不需要设置为Navigation Static的游戏对象。要烘焙一个 NavMesh,请按照以下说明操作:
-
从
_SceneAssets游戏对象。 -
从
_SceneAssets。 -
以下截图显示了选中
_SceneAssets并显示静态下拉菜单(标记为1.),然后取消选中Navigation Static(标记为2.):

图 12.6 – 取消选中 Navigation Static
- 一个窗口弹出,询问我们是否希望将更改应用于所有子对象。选择是,更改子对象。
因此,我们刚刚在level3场景中停用了所有环境艺术资产,以便它们不被用于导航烘焙。我们现在需要打开_SceneAssets层次结构中的其中一个子游戏对象:
-
从
corridorFloorNav:-
_SceneAssets -
level -
corridorFloor
-
以下截图显示了从corridorFloorNav(标记为1.):
-
在选择
corridorFloorNav时,确保在Inspector窗口中选中其Mesh Renderer组件(标记为2.)。 -
最后,为这个游戏对象选择Navigation Static(标记为3.):

图 12.7 – 打开'corridorFloorNav'游戏对象、网格渲染器和'Navigation Static'层
我们现在需要检查CorridorFloorNav网格。
- 在 Unity 编辑器的顶部选择窗口,然后点击AI | 导航。
很可能,导航窗口会出现在编辑器的右上角。如果它没有出现,并且作为浮动窗口出现在 Unity 编辑器的某个地方,只需单击并按住导航选项卡,并将其停靠在检查器选项卡旁边,如图下所示:

图 12.8 – 导航窗口
-
在导航窗口中,点击顶部的烘焙按钮,以获取我们的导航烘焙选项。
更多信息
还值得注意的是,在 NavMesh 中操作的游戏对象被称为代理。
在这个窗口中,我们看到了一系列用于导航烘焙的选项。一开始这可能看起来有些令人畏惧,但蓝色圆柱体基本上是我们的代理(逃跑的敌人)以及以下参数是基于我们的代理在导航路径上的灵活性。让我们简要地浏览每个选项,以便在我们烘焙之前了解其功能:
-
代理半径:这将围绕我们的代理创建一个不可见的护盾,使他们不能与其他代理、墙壁、门等发生碰撞。
-
代理高度:类似于代理半径,这给我们的代理一个不可见的高度;这对于 NavMesh 正在操作的游戏对象通过门很有用。
-
最大坡度:我们可以以度为单位调整代理可以爬上的坡度。
-
步高:这与最大坡度属性类似,但在这个情况下,它控制我们的代理可以移动多高的一步/楼梯。
-
下落高度:输入角色可以下落的最大高度(与Off Mesh Link组件相关联)。
-
跳跃距离:这指定了角色和对象之间的跳跃距离(与Off Mesh Link组件相关联)。
更多信息
关于Off Mesh Link组件的信息可以在
docs.unity3d.com/Manual/class-OffMeshLink.html找到。 -
手动体素:体素是一种三维测量,用于调整我们导航烘焙的精度。
-
体素大小:如果勾选了手动体素选项,这意味着我们可以为每个代理提供更紧密的精度。数字越低,我们的代理将越精确;请注意,这将使 NavMesh 烘焙时间更长。
-
最小区域面积:这指定了一个表面必须具有的最小面积,才能被包含在 NavMesh 中。
-
高度网格:此复选框将创建一个高度网格,这将提高移动精度。这也会使导航烘焙变慢。
以下截图显示了刚刚经过的导航烘焙设置:

图 12.9 – 选择“烘焙”标签的导航窗口
幸运的是,我们默认设置窗口的烘焙属性将按原样正常工作。
- 点击导航窗口右下角的烘焙按钮,等待编辑器右下角的仪表完成,如下截图所示:

图 12.10 – 为你的 NavMesh 导出瓦片
一旦导航烘焙完成,我们场景窗口中的corridorFloorNav将会有一个位于其网格上的 NavMesh。
如果你看不到导航烘焙的网格,请确保在网格右下角的显示 NavMesh复选框被勾选。以下截图显示了我们的 NavMesh 和Navmesh 显示框:

图 12.11 – 我们的导航网格及其在“场景”窗口右下角显示
在本节中最后要做的就是关闭corridorFloorNav游戏对象的网格渲染器组件。我们只需要这个组件处于活动状态,以便烘焙 NavMesh。
要关闭corridorFloorNav游戏对象的网格渲染器组件,请执行以下操作:
-
在层次结构窗口中选择
corridorFloorNav游戏对象。 -
在检查器窗口中,取消选中网格渲染器组件。
以下截图显示了需要取消选中的高亮框:

图 12.12 – 取消选中'corridorFloorNav'网格
这就是允许我们的逃跑敌人四处移动所需的所有内容。
更多信息
如果你想了解更多关于导航窗口的信息,请查看docs.unity3d.com/Manual/Navigation.html。
到目前为止,在本节中,我们讨论了 AI 的要求以及它在游戏中的应用,以及我们将如何使用 Unity 提供的标准 NavMesh 系统来应用这些方法到我们的逃跑敌人上。
现在我们已经为我们的代理烘焙了 NavMesh,以便它们可以移动,我们可以在下一节中查看设置我们的NavMeshAgent组件,为我们的代理设置一个固定的速度、加速度、停止距离等。
定制我们的代理 – NavMeshAgent
在本节中,我们将继续设置 NavMesh,但将重点转向代理(逃跑敌人游戏对象)。代理将在烘焙的 NavMesh 上移动。
逃跑敌人游戏对象需要能够在 NavMesh 内部做出反应和移动,同时也能以适合我们想要实现的行为的方式移动。例如,敌人需要带有恐慌的逃跑;因此,我们需要考虑诸如敌人何时决定移动、敌人反应速度有多快以及敌人移动速度有多快等特征。这些属性以及更多,都来自于一个名为 NavMeshAgent 的组件。
NavMeshAgent 是一个必需的组件,它将被附加到每个逃跑敌人游戏对象上。这个组件的目的是使游戏对象被识别为代理,并保持在 NavMesh 上。
在我们将 NavMeshAgent 添加到逃跑敌人之前,我们需要创建一个敌人预制件,这样我们就有了一个可以抓取和克隆多个敌人副本的地方:
-
从
Assets/Model文件夹中拖动enemy_flee.fbx到 Hierarchy 窗口的底部。 -
从
Assets/Prefab/Enemies中拖动enemy_flee。当窗口弹出询问是否要选择变体或原始时,选择原始。因为这种敌人只有一种类型。
这样我们的逃跑敌人就创建完成了;现在,我们可以通过以下步骤为其应用材质:
-
导航到
Assets/Prefab/Enemies文件夹并选择enemy_flee预制件。 -
从 Inspector 窗口中,选择 Mesh Renderer 组件的远程按钮(以下屏幕截图标记为 1)。
-
如果你在列表中看不到材质,请在顶部的搜索栏中输入
darkRed。 -
双击下拉菜单中的
darkRed(以下标记为 2)。 -
在这一点上,请确保
0值和1值。
以下屏幕截图显示了带有更新材质和正确 Transform 值的 enemy_flee 预制件:

图 12.13 – 将 'darkRed' 材质添加到 'enemy_flee' 网格渲染器材质槽中
你可能已经注意到在前一个屏幕截图中,enemy_flee 有硬朗、闪亮的边缘。我们可以在三维模型导入设置中通过以下方式使这些边缘看起来更平滑:
-
从
Assets/Model文件夹中选择enemy_flee。 -
在 Inspector 窗口中,将 Normals 属性值从 Import 更改为 Calculate。
我们现在可以通过滑动条调整 Smoothing Angle 值来改变角度之间的平滑度,如图所示:

图 12.14 – 平滑我们的敌人边缘
在前面的截图中,你可以看到制作模型看起来更平滑的三个不同阶段。这可以通过任何导入到 Unity 中的三维模型来完成。
- 一旦你对 平滑角度 值满意,点击 检查器 窗口右下角的 应用。
返回到 层次 窗口的 enemy_flee,因为这是一个敌人,我们还需要给它一个 敌人 标签,以便玩家在碰撞时能识别它:
-
点击 检查器 窗口顶部的 标签 参数。
-
选择 敌人。
以下截图显示了 enemy_flee:

图 12.17 – 'Nav Mesh Agent' 设置的值
- 在 检查器 窗口的右上角点击 覆盖 | 应用全部 以确认您的预制体更改。
在本节中,我们创建了逃跑的敌人预制体并为其添加了材质。我们还为我们的敌人应用了一个 NavMeshAgent 组件,使其校准并准备好反应。
以下截图显示了逃跑的敌人及其 NavMeshAgent 组件包裹的样子,这只能在 场景 窗口中看到:

图 12.18 – 我们带有 NavMeshAgent 碰撞体的敌人
在下一节中,我们将为逃跑的敌人预制体添加一个碰撞体,以便当玩家接触它时,玩家和敌人将随着即将到来的脚本被销毁。
为我们的逃跑敌人添加胶囊碰撞体
在本节中,我们将为逃跑的敌人添加一个胶囊碰撞体,以便在它们相互碰撞时检测到来自玩家飞船的碰撞:
-
在
enemy_flee预制体仍然被选中的情况下,滚动到 检查器 窗口的底部并点击 添加组件。 -
在下拉窗口中开始输入
Capsule,直到你看到 胶囊碰撞体。 -
从下拉列表中选择 胶囊碰撞体。逃跑的敌人现在将有一个胶囊碰撞体包裹在其周围。
-
最后,在 胶囊碰撞体 组件中勾选 是触发器 复选框。
-
点击
enemy_flee预制体的设置。 -
在 层次结构 中选择
enemy_flee游戏对象并删除它。
以下截图显示了 enemy_flee 带有其胶囊碰撞体;这些值可能与你不同:

图 12.19 – 带其胶囊碰撞体的 'enemy_flee'
逃跑的敌人几乎准备好在游戏中尝试了。我们只需要添加一个脚本,告诉游戏对象当它接近玩家一定距离时该做什么。我们将在下一节中介绍这一点。
创建我们的逃跑敌人脚本
在本节中,我们将使逃跑的敌人能够检测到玩家是否靠近。如果玩家太近,我们将使敌人开始逃跑。
我们将取一个部分完成的脚本并将其导入到本章,因为EnemyFlee脚本的大部分内容将与我们之前制作的相同设置,即第二章**,添加和操作对象。按照以下步骤操作:
-
从
Assets/Script。 -
双击
EnemyFlee脚本以开始添加其导航代码。
EnemyFlee脚本将包含类似于EnemyWave脚本的代码。我们的游戏中的敌人将具有相同的属性,例如被击中或死亡时给予和接受伤害、检测碰撞以及继承它们自己的可脚本化对象资产。实际上没有必要再次进行这个过程。我们感兴趣的是enemy_flee游戏对象的行为。
要将逃跑行为添加到EnemyFlee脚本中,我们需要执行以下步骤:
-
在脚本顶部添加 AI 库,以便我们的脚本可以访问导航代理文件:
using UnityEngine.AI;
在我们的脚本中,我们需要访问NavMeshAgent组件(它附加到我们的enemy_flee游戏对象上)。AI 库为我们提供了这个功能。
-
在脚本中向下滚动到我们的全局变量位置(
health、travelSpeed、fireRate等),并添加以下我们将要使用的变量,这些变量将用于我们的导航设置:GameObject player; bool gameStarts = false; [SerializeField] float enemyDistanceRun = 200; NavMeshAgent enemyAgent;
第一个变量将用于存储对玩家飞船的引用,因为我们将在稍后比较其距离。布尔值将作为我们脚本延迟启动的一部分使用。我们稍后会更详细地讨论这一点。
enemyDistanceRun将用作规则,在玩家和我们的逃跑敌人之间的测量距离内“行动”。我们还添加了SerializeField属性,因为它在检查器窗口中更改这些值时将非常方便,同时保持这个变量为私有。如果你认为 200 太低,可以尝试不同的值。实验一下吧!
最后,我们有NavMeshAgent,它将需要接收来自玩家和逃跑敌人的数据。
-
创建一个
Start函数,它需要一个短暂的延迟来从玩家飞船获取引用。输入以下代码。我们将逐步分析为什么会有延迟以及标准的ActorStats方法:void Start() { ActorStats(actorModel); Invoke("DelayedStart",0.5f); } void DelayedStart() { gameStarts = true; player = GameObject.FindGameObjectWithTag ("Player"); enemyAgent = GetComponent<NavMeshAgent>(); }
Start函数包含ActorStats方法,它将更新我们的敌人能力(如健康值、添加到分数中的点数等),类似于我们的enemy_wave游戏对象。我们还将运行一个Invoke函数,该函数接受我们希望运行的方法的名称以及一个参数,用于确定方法将在何时运行。
我们运行了一个短暂的0.5f秒延迟,给玩家的飞船时间在我们在它上面获取引用之前实例化到场景中。我们设置一个布尔值为true,表示update函数可以运行其中的内容,我们将在稍后讨论。我们做的最后一件事是从附加到游戏对象的NavMeshAgent组件中获取引用。
我们需要在ActorStats方法中的速度值上进行轻微的修改。因为我们正在影响NavMeshAgent_speed,所以我们需要直接操作它。
为了使敌人的速度可调节,请在EnemyFlee脚本的ActorStats中添加以下代码行:
GetComponent<NavMeshAgent>().speed = actorModel.speed;
敌人逃跑的速度值现在已连接。
-
继续到我们代码的最后一部分,
Update函数将测量并响应逃跑的敌人和玩家之间的距离。输入以下Update函数及其内容,我们将逐步讲解:void Update () { if(gameStarts) { if (player != null) { float distance = Vector3.Distance(transform.position,player.transform.position); player.transform.position); if (distance < enemyDistanceRun) { Vector3 disToPlayer = transform.position - player.transform.position; Vector3 newPos = transform.position + disToPlayer; enemyAgent.SetDestination(newPos); } } } }
在Update函数中,我们将运行一个if语句来检查gameStarts布尔值是否为true;如果是true,我们接着检查player_ship是否仍在场景中。如果是true,我们继续执行该if语句中的内容。在这个if语句中,我们使用Vector3.Distance来测量玩家飞船和逃跑的敌人之间的距离。然后我们将测量结果存储为一个名为distance的float值。
接下来,我们运行一个检查,看看测量的距离是否小于enemyDistanceRun值,该值目前设置为200。
如果distance变量的值较低,那么这意味着玩家的飞船离逃跑的敌人太近,因此我们需要执行以下步骤来使其做出反应。
-
存储减去玩家位置的
Vector3变量。 -
然后我们将这个
Vector3变量添加到逃跑的敌人的newPos位置,这将是指引敌人逃跑的方向。 -
最后,我们将这个
newPos位置发送到NavMeshAgent。 -
保存
EnemyFlee脚本。
现在我们准备将EnemyFlee脚本附加到我们的enemy_flee预制体上。让我们现在就做这件事;然后,我们将能够测试结果:
-
在 Unity 编辑器中,导航到项目窗口中的
Assets/Prefab/Enemies文件夹。 -
选择
enemy_flee预制体。 -
点击
EnemyFlee。 -
从下拉列表中选择
Enemy Flee脚本。 -
创建一个新的
BasicFlee_Enemy,并将其存储在Assets/ScriptableObject中。将 Actor 拖入检查器窗口中EnemyFlee脚本的 Actor Model 区域,如图所示。
以下截图显示了EnemyFlee脚本的可脚本化对象资产在右侧的Actor Model参数:


图 12.20 – 持有其'EnemyFlee'脚本和更新字段的'enemy_flee'游戏对象
现在,我们需要使我们的enemy_flee脚本在游戏 HUD 中的雷达图上可识别,就像enemy_wave游戏对象一样。作为提醒,我们在第九章,创建 2D 商店界面和游戏内 HUD中创建了一个radarPoint对象。因此,在本章中,我们将加快速度,并使用一个现成的radarPoint对象将其附加到enemy_flee游戏对象上。与现成的radarPoint游戏对象唯一的不同之处在于,我附加了一个名为RadarRotation的小脚本,该脚本将使radarPoint精灵始终面向相机,无论enemy_flee游戏对象进行何种旋转。
RadarRotation脚本在Awake函数中获取当前旋转,然后在LateUpdate中重新应用旋转。
什么是LateUpdate?
LateUpdate是 Unity 执行顺序中最后调用的函数。这样做的好处是,radarPoint对象的旋转和同时被调用的enemy_flee旋转之间没有冲突。如果您想了解更多关于执行顺序的信息,请查看docs.unity3d.com/Manual/ExecutionOrder.html。
要将预制的radarPoint对象附加到enemy_flee预制体上,我们需要执行以下操作:
-
返回
enemy_flee预制体,从Assets/Prefab/Enemies到层次结构窗口。 -
将
radarPoint对象从Assets/Prefab/Enemies拖放到层次结构窗口中的enemy_flee预制体上。 -
然后,将
RadarRotation脚本从Assets/Script拖放到enemy_flee的radarPoint对象上,使其指向相机。 -
一旦应用,从层次结构窗口中选择
enemy_flee预制体,然后点击覆盖 | 应用全部在检查器窗口的右上角。 -
以下截图显示了
enemy_flee预制体持有的radarPoint对象,以及检查器窗口中的radarPoint对象,作为避免任何错误的参考:

图 12.21 – ‘radarPoint’游戏对象及其变换属性值
- 我们的
enemy_flee预制体现在可以从其当前位置在enemy_flee中试验到_Enemies游戏对象,现在在层次结构窗口中:

图 12.22 – 层次结构窗口中‘Enemies’游戏对象的子对象
- 将
enemy_flee预制体放置在关卡开始附近的位置。我将其放置在以下变换值中:

图 12.23 – 第 3 级中‘enemy_flee’游戏对象的位置
如果你也在关卡开始附近有 EnemySpawner 对象,将其沿 x 轴推回直到 1000,以便将其移开。
- 现在点击
enemy_flee对象应该开始恐慌并四处移动,试图逃离你!
你可以自由地在 层次结构 窗口中选择 enemy_flee 对象,然后按键盘上的 左键 + Ctrl (Command 在 macOS 上) + D,将几个逃跑的敌人分散到周围,使关卡更有趣,如以下截图所示:

图 12.24 – 复制的 'enemy_flee' 游戏对象
以下截图显示了我们的新逃跑敌人试图在纯粹的恐慌中逃离玩家!

图 12.25 – 敌人从玩家那里逃跑!
- 保存场景。
这部分内容到此结束,希望你现在对使用 NavMesh 和代理的介绍感到舒适。正如你可以想象的那样,我们的逃跑敌人可能还有其他事件与之关联,例如在安全距离向玩家射击子弹、在角落周围躲避,以及呼救。给一个 NPC 添加一系列事件需要有限状态机来遍历每个适当的事件。
在本节中,我们介绍了一个与当前波次敌人行为不同的新敌人。我们还熟悉了 Unity 提供的现成路径查找算法,如 NavMesh。
我们将进入下一节,我们将介绍时间轴,它作为一个动画师工作,但也可以与我们的组件结合使用,例如,通过脚本使灯光融合成不同的颜色。
探索时间轴
时间轴是 Unity 编辑器中的一个组件,旨在将一系列动画组合在一起,这对电影和电视等行业很有吸引力。时间轴也受到可能包含场景的游戏的欢迎,以帮助讲述故事或向玩家介绍关卡。Unity 还有两个其他有用的组件——动画控制器和动画剪辑——如果你一直在跟随这本书,你应该知道,因为我们已经在 第四章 应用艺术、动画和粒子 中介绍了这些其他组件。它们执行相同的任务,但随着场景变得越来越繁忙,一系列单独的动画剪辑,动画控制器中可能会变得混乱,多个状态之间相互转换。
以下截图显示了具有多个状态和转换的动画控制器:

图 12.26 – 动画控制器可能变得多么混乱的示例
时间轴支持三个任务:
-
播放动画和剪辑
-
播放音频
-
打开或关闭游戏对象
这三个功能本身限制了时间轴——例如,如果我们想改变光线的颜色,时间轴就无法单独改变单个属性。要改变光线的颜色,我们需要在动画窗口本身中改变光线的属性值。然而,通过在时间轴上添加一些额外的脚本,我们可以引入拖放带有组件的游戏对象,例如灯光组件,这样就可以在飞行中做出更改。
在本节中,我们将首先在时间轴中动画化一个大型机器人飞船。然后,我们将讨论可玩性以及它们如何扩展时间轴的功能。最后,我们将向时间轴添加额外的轨道来控制灯光的颜色,并在玩家到达关卡末尾时将关卡淡入黑暗。
让我们从创建我们的时间轴游戏对象并添加一个时间轴组件到它开始。
创建时间轴
在本节中,我们将更熟悉时间轴组件,并使用一个大型飞行机器人创建一些自己的动画。在设置时间轴时,我们还将讨论涉及的组件和属性。
要将时间轴添加到场景中,请执行以下操作:
-
在 Unity 编辑器的
Assets/Scene。 -
双击
level3以加载场景,如果尚未加载。 -
在层次结构窗口中右键单击,并从下拉菜单中选择创建空对象以创建一个空的游戏对象。
-
慢慢点击
GameObject两次以重命名它。 -
将
GameObject重命名为时间轴。
在层次结构窗口中仍然选择我们的时间轴游戏对象,我们现在可以打开我们的时间轴窗口。
在 Unity 编辑器顶部,点击窗口 | 序列然后时间轴;以下截图显示了这一点:

图 12.27 – 选择 '时间轴' 游戏对象并打开时间轴窗口
很可能时间轴窗口将出现在与你的场景相同的窗口布局中,这并不理想,因为我们希望在动画时看到我们的场景。要将时间轴窗口移动到更好的位置,请点击时间轴标签的名称并将其拖到屏幕底部,那里有控制台和项目窗口。以下截图显示了我当前的 Unity 编辑器布局比例:

图 12.28 – Unity 编辑器中的窗口布局
我们现在可以继续创建我们的时间轴资产,其中我们将为所有游戏对象及其组件创建新的动画。
要创建一个可玩的时间轴资产,请执行以下操作:
-
在层次结构窗口中仍然选择
时间轴游戏对象,点击时间轴窗口中的创建按钮。 -
将出现一个窗口浏览器,让我们选择保存可播放文件的位置。
-
选择
Assets文件夹。 -
给可播放文件命名(与它将要被用于的内容相关;我将其命名为
level3)并点击 Save 按钮。
我们已创建 Timeline 资产。
如果你一直跟随这本书,那么乍一看,Timeline 窗口可能会看起来像我们在 第四章“应用艺术、动画和粒子”中看到的 Animation 窗口。如果是这样,那就很好!控制部分和方法的一部分将对你来说很熟悉。Timeline 的主要区别之一是,任何游戏对象都可以拖放到 Timeline 窗口中,而无需它们之间有任何层次关系。
以下截图显示了我们的 Timeline 窗口,其中包含在第一个 Timeline 轨道中的 Timeline 游戏对象:

图 12.29 – 时间线窗口
此外,在 Inspector 窗口中,我们的 Timeline 游戏对象增加了一些额外的组件。以下截图显示了添加的两个组件 – Playable Director 和 Animator:

图 12.30 – 在检查器窗口中持有 Playable Director 和 Animator 组件的 'Timeline' 游戏对象
我们在 第四章“应用艺术、动画和粒子”中使用了 Animator 组件,因此有关此特定组件的更多详细信息,请参阅该章节。此外,我们实际上并没有对 Animator 组件做任何事情;它只是我们 Timeline 设置中所需的一个组件。
创建时间线资产文件时,我们获得的另一个组件是 Playable Director。该组件的职责是保持时间线与正在操作的游戏对象/组件之间的关系。因此,让我们简要了解 Playable Director 组件下的每个属性,以获得一般了解。
首先,我们有 Playable。当我们点击 Timeline 窗口中的 Create 按钮时,我们创建一个 Playable 文件。此文件包含与时间线相关的所有动画和游戏对象/组件实例。
然后,我们有 Update Method。此参数提供了四个属性,用于控制时间如何影响时间线。这些属性如下:
-
数字信号处理 (DSP) 有助于提高时间线与音频之间的准确性,以防止它们不同步。
-
游戏时间:时间线的来源将是游戏的时间。这也意味着时间可以进行缩放(即减速或暂停)。
-
未缩放的游戏时间:此选项与游戏时间属性的工作方式相同,但它不受缩放的影响。
-
手动:此属性使用我们通过脚本提供的时钟时间。
接下来,我们有在唤醒时播放。如果这个复选框被勾选,我们的时间轴将在场景激活时立即播放。
下一个参数是循环模式。此属性确定时间轴播放完毕后会发生什么:
-
保持:当时间轴到达末尾时,它会停留在最后一帧。
-
循环:时间轴重复。
-
无:时间轴播放后重置。
初始时间在时间轴开始之前添加延迟(以秒为单位)。
最后,我们有绑定。当一个游戏对象或组件被拖入时间轴窗口时,绑定列表将更新并显示连接到时间轴的对象。
到目前为止,我们已经讨论了时间轴并将其引入到我们的场景中。我们还介绍了使时间轴工作所需的组件。
现在我们对时间轴及其协同工作的组件更加熟悉,在接下来的小节中,我们将把我们的大型 Boss 飞船整合到level3场景中,并通过时间轴对其进行动画处理。
在场景中设置 Boss 游戏对象
在本节中,我们将从导入的项目文件中取出一个静态的类似 UFO 的游戏对象,将其拖入场景,并将其附加到时间轴上。从那里,我们将对 UFO 进行动画处理,使其在两个场合旋转并在场景中移动。
要在动画之前将大型 Boss UFO 游戏对象引入我们的场景,我们需要做以下操作:
- 将
Assets/Prefab/Enemies中的boss.prefab对象拖放到层次结构窗口中的_Enemies游戏对象。
接下来,我们需要定位 Boss,使其在我们的场景中但不在摄像机的视野中。这样,当在时间轴中动画 Boss 时,我们可以根据需要更改其位置和旋转。
-
在
0,0,和-2000处选择boss游戏对象 -
0,0,和0 -
1,1,和1 -
在层次结构窗口中仍然选择
boss对象时,按键盘上的F键,以查看它在场景窗口中的样子。
以下截图显示了导入的boss预制件,其中包含组件列表和属性值:
![图 12.31 – 标记为'Enemy'的'boss'游戏对象,使用变换组件定位,最后设置为具有缩放半径的触发器]
![img/Figure_12.31_B18381.jpg]
图 12.31 – 标记为'Enemy'的'boss'游戏对象,使用变换组件定位,最后设置为具有缩放半径的触发器
如前一个截图所示,boss对象包含以下组件和属性值:
-
标记为Enemy(用1表示)。
-
变换属性值设置为步骤 2(用2表示)中详细说明的值。
-
80(用3表示)。 -
BossScript使boss游戏对象对玩家不可见,如果玩家与 boss 接触,玩家将死亡(用4.表示)。 -
因为
boss对象是一个敌人,它有一个在雷达上被拾取的radarPoint对象(用5.表示)。
在我们进入下一节之前,我们需要将一个RadarRotation脚本添加到radarPoint游戏对象中,该对象是boss游戏对象的子对象。这个脚本将使radarPoint始终面向摄像机:
-
在层次结构窗口中展开
boss内容。 -
选择
radarPoint,然后从Assets/Script拖动并放下RadarRotation脚本,将其从项目窗口移动到检查器窗口。 -
最后,从
Assets/Script将BossScript中的boss游戏对象拖入检查器窗口。
现在boss对象已经在场景中,我们可以在下一节中将其添加到时间轴中。
为时间轴准备 boss
在本节中,我们将从boss对象中取出boss游戏对象,在玩家到达关卡末尾并离开之前向玩家打招呼。
后续章节将继续使用时间轴,包括使用专门的boss对象动画。
要在时间轴上动画化boss对象,请执行以下操作:
- 选择
Timeline游戏对象,并在检查器窗口中取消勾选在唤醒时播放,因为我们自己将触发Timeline动画。
要触发我们在本章开头提到的BossTrigger游戏对象。
要使主摄像机被识别为触发器,我们需要执行以下操作:
-
在层次结构窗口中选择主摄像机。
-
点击检查器窗口中的添加组件按钮。
-
输入
Box Collider,当你在下拉列表中看到它时,选择它。 -
在Box Collider组件下勾选是触发器框。
让我们现在继续设置我们的boss游戏对象:
- 选择Timeline选项卡,如果您一直跟随前面的章节,您会知道它位于 Unity 编辑器的底部
Timeline游戏对象和从Timeline窗口中的Timeline游戏对象,因为我们不会对它进行动画化。
- 在Timeline窗口中右键单击
Timeline对象,并从下拉菜单中选择删除。
以下截图显示了正在删除的Timeline游戏对象:

图 12.32 – 从时间轴窗口中删除'Timeline'游戏对象
- 在层次结构窗口中仍然选择
boss游戏对象,并将其从Timeline窗口向下拖入。
将出现一个下拉菜单,有三个选择项:
-
激活轨道:打开或关闭游戏对象
-
动画轨道:动画化游戏对象
-
音频轨道:打开或关闭特定的音频
- 因为我们想要对
boss游戏对象进行动画处理,所以我们将选择动画轨道。
现在,我们的boss游戏对象将在检查器窗口中获得一个动画器组件。
以下截图显示了我们的时间轴当前的外观:

图 12.33 – 持有‘boss’游戏对象的时间轴
接下来,我们将开始在时间轴窗口中添加关键帧,这将影响我们的 boss 对象的位置和旋转。让我们首先锁定我们的时间轴窗口,这样当我们点击另一个游戏对象时,它将保持活动状态:
-
在层次结构窗口中选择
Timeline游戏对象。 -
选择时间轴窗口标签。
-
点击时间轴窗口右上角的小锁按钮。
以下截图突出显示了小锁按钮:

图 12.34 – 锁定时间轴窗口
现在让我们继续下一节,我们将开始在时间轴中添加关键帧,并使我们的boss游戏对象在第三级的两个阶段中移动和旋转。让我们从第一阶段开始。
在时间轴中动画化 boss – 第一阶段
在本节中,我们将为boss游戏对象添加关键帧。这将使boss游戏对象在其中心轴上旋转的同时从一个点到另一个点移动。
要为boss游戏对象添加关键帧,请执行以下操作:
- 与
boss游戏对象一起。
现在,我们将开始记录 boss 的位置和旋转。
-
在
boss游戏对象名称中;按钮应该开始闪烁。 -
确保如以下截图所示的
0:

图 12.35 – 时间轴帧设置为零
-
在
1675、0和600 -
60、-90和0

图 12.36 – ‘boss’游戏对象在我们 3 级场景中的当前位置
现在,为了使boss对象从走廊的一端移动到另一端,我们需要为boss添加另一个关键帧。按照以下步骤操作:
-
当录音按钮仍然闪烁在
112或更改112的值。 -
在
3160、0和600中选择boss游戏对象 -
60、-90和20进一步信息
时间轴和动画窗口在缩放和滚动方面遵循相同的导航规则。按住鼠标中键并移动鼠标可以进行滚动。通过上下滚动鼠标中键可以放大或缩小。将鼠标光标悬停在动画条上并按键盘上的F键,可以在窗口中显示所有关键帧。
-
点击时间轴窗口中
boss游戏对象旁边的录音按钮以停止录音。 -
点击并拖动(
boss游戏对象从左到右移动并旋转)。
以下截图显示了boss游戏对象从左向右移动的鸟瞰图:

图 12.37 – 'boss'游戏对象将从左向右移动
在稍后,当我们玩第三级时,我们将看到boss游戏对象从远处冲过玩家的那一刻。现在,我们将在查看可玩性之前,继续在我们的时间轴窗口中添加更多关键帧。
让我们继续进行动画化我们的boss游戏对象的第二阶段。
在时间轴中动画化老板 – 第二阶段
在本节中,我们将在关卡结束时对老板进行第二次动画,作为对第三个和最后一个关卡结束的一种形式的解决。
我们将继续从上一节开始的同时间轴轨道进行。
因此,让我们从我们离开的地方继续动画化我们的老板:
-
保持时间轴窗口锁定,以防止窗口丢失其显示。
-
从层次结构窗口中选择
boss对象。 -
在时间轴窗口中,按住
boss对象名称旁边的记录按钮,使其闪烁。 -
在帧参数中输入
1012。
在boss对象的位置和旋转值仍然被选中时,将其移动到以下:
-
4545、0和600 -
60、-90和0
以下截图显示了我们的老板在第二阶段的位置:

图 12.38 – 'boss'游戏对象的位置
-
在记录按钮仍然闪烁的情况下,将帧移动到
boss游戏对象的1180帧,到以下6390、0和600。 -
60、-90和20 -
现在,将
boss游戏对象的帧移动到1193,到以下6390、0和207。 -
60、450和0 -
现在,将
boss游戏对象的帧移动到1215,到以下5520、0和50。 -
60、90和-40 -
现在,将
boss游戏对象的帧移动到1380,到以下5510、0和50。 -
60、90和0 -
现在,将
boss游戏对象的帧移动到1400,到以下5510、0和50。 -
60、-70和-40 -
现在,将
boss游戏对象的帧移动到1420,到以下7540、0和50。 -
60、-70和0 -
在时间轴窗口中按
boss游戏对象停止录制。
以下截图显示了每个这些位置及其时间轴帧编号的鸟瞰图:

图 12.39 – 'boss'游戏对象将出现的位置的时间轴帧编号
如果您想调整 boss 游戏对象的旋转,建议在 Hierarchy 窗口中选中 boss。确保选中了本地位置(以下截图中的 1 所示),并且,在时间轴仍然处于录制模式时,旋转 z 轴几次(以下截图中的 2 所示)。

图 12.40 – 旋转 'boss' 游戏对象
最后,在 Timeline 中前后滚动(移动 Timeline 指示器)以查看您得到的结果。
当您感到高兴时,停止录制。
如您在前面的截图中所见,boss 游戏对象从左向右飞来,朝向玩家所在的位置。boss 将停止旋转,暂停,转身,然后飞向最右边。
让我们按下 level3 场景,直到 boss 与玩家飞船在整个 level3 场景中对抗。
玩家穿过关卡,boss 动画出现在两次。我们可能看到的是,即使 boss 已经移动,boss 的那个大黄色点仍然存在。如果我们能在看不到游戏对象本身时让 boss 从雷达上消失,那就更好了。
以下截图显示了左屏幕上的 boss 对象,它在右屏幕上不可见,但仍然被雷达检测到:

图 12.41 – 'boss' 游戏对象的雷达仍在检测 'boss'
在结束本节之前,让我们利用时间轴来简单地关闭 radarPoint 游戏对象,并在我们看到 boss 游戏对象时重新打开它。按照以下步骤操作:
-
从 Hierarchy 窗口中选择
Timeline游戏对象。 -
选择
Timeline选项卡以查看boss动画。 -
从 Hierarchy 窗口中将
boss的radarPoint对象拖动到 Timeline 窗口中。 -
Timeline 下拉菜单出现。这次,选择 Activation Track。
-
我们的
radarPoint轨迹。
现在让我们添加一个激活剪辑来决定玩家何时应该看到以及何时不应该看到 radarPoint 对象。按照以下步骤操作:
- 右键单击
radarPoint对象的轨道,并选择 Add Activation Clip,如下面的截图所示:

图 12.42 – 在时间轴窗口中,为我们的 'radarPoint' 游戏对象添加一个激活剪辑
-
一个
boss游戏对象在两次出现时应该是活跃的——一次是在boss穿过环境中的开放空间区域时,另一次是在结束时boss正面接近玩家时。 -
对于第一次出现,我们需要在时间轴上设置
35和95。我们可以通过点击并拖动其条形到95标记来完成此操作,如下面的截图所示:

图 12.43 – 在设置帧之间设置'radarPoint'游戏对象的活动
在第二次操作中,我们可以将boss对象从大约1020设置为1420。
通过以下步骤重复此过程:
-
右键单击并在
radarPoint轨道上创建一个激活剪辑。 -
调整两个时间轴点之间的活动条。
-
保存场景。
我们现在已设置设置,使boss游戏对象及其radarPoint对象同时处于活动状态。
我们已经成功地将时间轴引入到场景中,并对其进行定制,使其能够容纳在整个游戏的第三级和最终级中需要动画化的新游戏对象。在下一节中,我们将进一步探讨通过引入动画灯光来扩展时间轴的功能。
扩展时间轴
在本节中,我们将通过增加标准轨道选择来为时间轴添加更多功能,如下截图所示:

图 12.44 – 下一步将扩展我们时间轴的功能
从上一张截图所示的新扩展轨道选择列表中,我们将使用灯光控制轨道。
可以从下拉列表中看到更大的选择;然而,这超出了本书的范围。但是,如果您感兴趣,我将在本节稍后指导您如何扩展列表。
在接下来的几节中,我们将借助资产商店增加我们的轨道列表,并从 Unity 下载一个免费资产来增加我们的时间轴功能。然后,我们将对场景中的灯光进行动画处理,这在之前是不可能的。
将默认可播放内容添加到项目中
在本节中,我们将通过访问 Unity 资产商店并下载一个名为默认可播放内容的免费包来简化脚本编写,以扩展时间轴的功能。我们将讨论可播放内容的主要功能和它们包含的内容,但这太长了,不能作为脚本方法来讨论。
可播放内容通过可播放图来组织、混合和融合数据,以创建单个输出。要下载并导入默认可播放内容到我们的列表中,请执行以下操作:
-
打开您的网络浏览器并转到 Unity 资产商店:
assetstore.unity.com/。 -
在
default playables的顶部按下键盘上的Enter键。 -
从缩略图列表中选择唯一选项 – 默认可播放内容。
-
在默认可播放内容商店屏幕上,点击下载按钮。
-
下载完成后,我们可以通过点击以下截图所示的添加到我的资产按钮,将资产导入到我们的项目中:

图 12.45 – 从 Asset Store 将免费的默认可播放文件包导入到我们的项目中
默认可播放文件现在可以通过包管理器从您的 Unity 项目中下载。
包管理器为我们提供了访问从Asset Store下载的资产以及我们可以添加到项目中的其他“包”。我们将在下一章中详细介绍包管理器。
要将默认可播放文件下载到我们的项目中,我们需要执行以下操作:
-
在 Unity 编辑器中,点击窗口,然后选择包管理器。
-
包管理器将在新窗口中加载。在窗口的左上角,选择包,然后从下拉菜单中选择我的资产,如图下截图所示:

图 12.46 – 在包管理器窗口的下拉菜单中选择“我的资产”
我们现在将看到从 Asset Store 拥有的资产。
- 在
default playable的右上角以缩短左侧的列表,如图下截图所示:

图 12.47 – 在包管理器窗口右上角突出显示的搜索栏
- 在窗口的左侧选择默认可播放文件,然后点击下载,接着点击窗口右下角的导入按钮。
我们将看到要导入到项目中的文件夹和文件列表。
- 点击如图下截图所示的导入按钮:

图 12.48 – 要导入到项目中的默认可播放文件
现在我们已经向我们的时间轴添加了额外的功能,在Assets文件夹中新增了一个名为DefaultPlayables的文件夹。此外,正如之前提到的,要向时间轴添加更多功能(如时间轴轨道),请查看DefaultPlayables文件夹内的名为DefaultPlayablesDocumentation的文件。
现在让我们进入下一节,我们将利用场景中灯光的操作。
在时间轴中操作灯光组件
在本节中,我们将继续在同一个时间轴上工作,并扩展它以容纳更多轨道。在上一个章节中,我们介绍了一个来自 Asset Store 的免费可下载资产,名为默认可播放文件,以避免从头编写代码并提供新的可播放文件。这个资产使我们能够向时间轴添加新轨道。为了继续添加新轨道,我们将操作第三级场景中的灯光。
要将灯光组件添加到时间轴,我们需要执行以下操作:
-
确保时间轴窗口仍然处于锁定状态,这是我们之前在为时间轴准备 BOSS部分设置的。
-
在时间轴窗口的左下角空白角落处右键单击并选择灯光控制轨道,如图下截图所示:
![Figure 12.49 – Adding 'Light Control Track' to the Timeline]
![img/Figure_12.49_B18381.jpg]
Figure 12.49 – Adding 'Light Control Track' to the Timeline
我们现在已经在时间轴中添加了一个空的光组件轨道。
- 接下来,我们可以通过在时间轴的轨道线上右键单击并选择添加灯光控制剪辑来添加一个动画剪辑,如图下截图所示:
![Figure 12.50 – Adding control to our lights with the control clip]
![img/Figure_12.50_B18381.jpg]
Figure 12.50 – Adding control to our lights with the control clip
- 我们现在在时间轴中有一个
LightControlClip对象。单击此剪辑并查看检查器窗口。这里有几个选项,但我们主要关注颜色、强度和范围。
这些属性将直接改变位于无(灯光)参数中的灯光的值。
- 在您的检查器窗口中将您的灯光控制剪辑值设置为以下截图所示:
![Figure 12.51 – Copying the values from your 'Light Control Clip' to the ones in this screenshot]
![img/Figure_12.51_B18381.jpg]
Figure 12.51 – Copying the values from your 'Light Control Clip' to the ones in this screenshot
- 接下来,我们将设置此剪辑的持续时间为
100。我们可以通过将LightControlClip的值更改为100标记来实现,如图下截图所示:
![Figure 12.52 – Setting the duration of the light control clip to 100]
![img/Figure_12.52_B18381.jpg]
Figure 12.52 – Setting the duration of the light control clip to 100
因为这个灯光将会从白色闪烁到红色,理想情况下,在两个过渡之间有一个循环是有意义的。然而,为了混合和填充时间轴,我们将这样做。
-
选择
LightControlClip并按Ctrl (command 在 macOS 上) + D 25 次,以在轨道线上填充灯光控制剪辑。 -
从左侧选择第二个
LightControlClip对象并将其颜色属性从白色更改为红色。 -
对剪辑
4、6、8、10、12、14、16、18、20、22、24和26重复此过程。 -
现在,放大第二个
LightControlClip对象并将其移动到其前一个剪辑的一半位置,以在灯光颜色之间创建混合,如图下截图所示:
![Figure 12.53 – 1st light clip splicing into the 2nd light clip]
![img/Figure_12.53_B18381.jpg]
Figure 12.53 – 1st light clip splicing into the 2nd light clip
- 将每个剪辑移动到上一个剪辑位置的一半,以使灯光从白色闪烁到红色,直到级别结束。以下截图显示了第三个剪辑的位置:
![Figure 12.54 – The 2nd light clip splicing into the 3rd]
![Figure 12.54_B18381.jpg]
Figure 12.54 – The 2nd light clip splicing into the 3rd
一旦我们合并了剪辑,现在我们可以复制我们的灯光轨道资产,这样就可以让多个灯光闪烁。
- 点击轨道资产,然后按Ctrl (Command 在 macOS 上) + D四次。以下截图使用*****符号突出显示点击位置,以及创建的副本:

图 12.55 – 复制轨道资产
由于这些都是相同类型的轨道,我们可以将它们放入一个轨道组中,以保持我们的时间轴整洁。
-
在时间轴底部左边的空白区域右键单击,并从下拉菜单中选择轨道组。
-
我们的轨道组已经创建好了。现在,按住Shift键的同时,点击并拖动顶部的灯光轨道资产,然后点击底部的灯光轨道资产以选择所有的灯光轨道资产。仍然按住鼠标左键,将这些轨道资产拖动到轨道组中。
-
点击
Lights。
您可以使用+按钮展开和折叠组。
以下截图显示了时间轴灯光的最终结果:

图 12.56 – 被称为'Lights'的轨道组,包含所有我们的灯光轨道资产
- 现在你已经知道如何创建轨道组了,按照相同的过程为
boss对象及其radarPoint对象创建轨道组,并将其命名为Boss。
最后一步是将将闪烁红白光的五个灯光拖放到游戏窗口中。
-
要为每个参数选择
light00、light01、light02、light03和light04旁边的小型圆形遥控按钮。 -
在时间轴上前后拖动或滑动时间轴指示器,以查看选定的灯光闪烁红色。
-
保存场景。
以下截图显示了我们的玩家在第 3 关,有一组新的 AI 敌人,一个大型 BOSS 在背景中飞行,以及闪烁的灯光:

图 12.57 – 第 3 关的中段
如果你还没有这样做,现在是一个很好的时机来应用暂停屏幕,对所有三个场景执行以下操作。如果你不习惯自己操作,请遵循以下说明:
-
将
level3场景保存后,从项目窗口的Assets/Scene文件夹中加载level1。 -
在
Canvas和EventSystem游戏对象中,然后按C键复制它们。 -
再次加载
level3场景。 -
从层次结构窗口中选择
Canvas游戏对象,然后在键盘上按Delete键。 -
按Ctrl (Command 在 macOS 上) + V粘贴来自包含暂停屏幕的场景的
EventSystem和Canvas。 -
在层次结构窗口中展开
Canvas,然后展开LevelTitle游戏对象。 -
选择
Level游戏对象,并在检查器窗口中将level 1更改为level3。 -
保存场景。
-
对
level2重复此过程。
以下截图显示了暂停的level3:

图 12.58 – 暂停屏幕在 3 级关卡中工作
让我们继续总结本章我们所学的内容。
摘要
在本章中,我们向我们的游戏引入了一个新概念,使其比仅仅发生在空间中更有趣。我们的摄像机和玩家需要稍作调整以支持最终关卡的水平滚动,而不是像前两个关卡那样在屏幕上保持静态。我们还引入了一个可以在地板上移动的第二个敌人,它在恐慌状态下躲避其他敌人。逃跑的敌人使用了 Unity 标准提供的导航系统,我们给了敌人一个可以跑动的地板。最后,我们引入了时间轴功能,这与我们在第四章**,应用艺术、动画和粒子中使用的原始动画系统相关。我们还发现了时间轴如何让任何游戏对象或组件在其中被动画化,而无需在游戏对象之间建立某种形式的层次链接。此外,我们还扩展了时间轴以涵盖其他组件,例如灯光,我们无法仅使用时间轴功能单独对它们进行动画化。
本章的主要收获是介绍了具有导航系统的人工智能,该系统也可以用于其他游戏中的其他行为,以及介绍了时间轴及其在项目(如过场动画、电影和动画电视连续剧)中鼓励创造性的使用。
在下一章中,我们将探讨如何打磨我们游戏的外观,并查看哪些工具可以帮助我们优化性能和测试错误。
在你进入下一章之前,尝试以下模拟测试,因为这是本书在大型模拟测试之前最后的迷你模拟测试。
模拟测试
- 你正在开发一个游戏,你的玩家在办公室里,周围有其他工作人员。当你的玩家走到一个特定点时,会调用一个触发事件,使用可玩导演将工作人员移动到另一个房间。
你会注意到,当游戏暂停然后取消暂停时,音频和动画彼此不同步。
在可玩导演组件中,哪个属性可能会解决这个问题?
-
将包装模式设置为保持。
-
将更新方法设置为DSP。
-
将初始时间设置为当前时间(游戏暂停时的时间)。
-
将更新方法设置为未缩放的游戏时间。
-
我们在我们的可玩图中有一些可玩元素链接。我们需要移除其中一个可玩元素及其输入。
我们应该使用哪个PlayerGraph函数?
-
DestroyOutput -
DestroyPlayable -
DestroySubgraph -
Destroy -
你开发了一个八球台球游戏。一位测试人员回到你这里,说当一名玩家在游戏开始时打碎球时,游戏的帧率会过低。所有的球都有刚体球体碰撞器。
我们如何提高帧率的下降?
-
在相互碰撞的对象上使用更便宜的着色器。
-
将最大允许的时间步长设置为 8-10 fps 的范围,以应对这种最坏的情况。
-
将刚体更改为运动学。
-
使用盒子和胶囊碰撞器而不是球体碰撞器。
-
NavMesh修改器做什么?-
一个
NavMesh修改器确定 NavMesh 在构建过程的哪个阶段进行烘焙。 -
一个
NavMesh修改器描述场景中每个代理的 AI。 -
一个
NavMesh修改器允许在主线程之外进行 NavMesh 烘焙,以便可以在运行时动态烘焙。 -
一个
NavMesh修改器调整游戏对象在 NavMesh 烘焙期间的行为,例如,可以仅影响某些代理。
-
-
为什么只选中必要的框在层碰撞矩阵中会有帮助?
-
取消选中框将隐藏层,以便它们不会被渲染。
-
选中框将指示哪些碰撞可以忽略。
-
选中框将在帧调试器中显示哪些层正在碰撞。
-
取消选中框将减少物理系统需要检查的层碰撞数量。
-
-
当我们为游戏对象创建一个
Timeline资产时,会创建并添加到我们的游戏对象的组件是什么?-
PlayableBinding -
PlayableDirector -
PlayableOutput -
PlayableGraph
-
-
在你正在测试的第一人称射击游戏中,你注意到当警报响起时,敌人守卫会跑向玩家。当你观察敌人守卫前进时,你关上了门,但注意到他们的手臂和头部正穿过你关上的门。
你需要增加哪个设置来停止这些手臂和头部穿过它们不应该穿过的物体?
-
步高
-
最大坡度
-
代理高度
-
代理半径
-
你已经卷入了一场经典的拯救市长救援游戏。你的玩家是一名受过训练的义警,试图消除可能攻击市长的潜在攻击者。其中一名攻击者离得太近,你开枪警告他们。攻击者跑开了,但不久后又回来,匍匐着向市长爬去。
哪个 NavMesh 代理属性可以模拟这种谨慎的行为?
-
区域遮罩
-
自动制动
-
停止距离
-
优先级
-
恭喜——拯救市长取得了巨大成功,你被要求立即开始开发拯救市长 2!你的义警回来了,这次他可以跳 和 跑,并跳过建筑物的屋顶。
再次,你已经应用了 NavMesh 代理,让你的义警可以在直线路径上跑和跳过建筑物。你已经正确连接了所有动画控制,但注意到你的义警在跳过建筑物屋顶时没有进行动画。
我们需要更改哪个设置或属性来解决此问题?
-
取消选中
NavMesh代理组件。 -
增加导航网格代理组件。
-
在导航下的烘焙设置中取消选中高度网格属性。
-
在导航下的烘焙设置中增加跳跃距离。
-
我们正在制作一款第三人称游戏,我们的角色正在使用有限状态机来对其状态做出反应。目前我们设置为,如果我们离某个角色太近,他们就会试图跑过来拥抱我们。
程序员正在工作的有限状态机组件是什么?
-
动作
-
过渡
-
事件
-
规则
-
哪些轨道在不需要额外编码的情况下,时间轴无法添加?
-
激活轨道
-
动画轨道
-
灯光控制轨道
-
可玩轨道
-
-
其中一位 3D 艺术家向您提供了一系列三维模型,这些模型将被应用于您目前正在开发的项目中的一个启动场景。
当将模型导入场景时,你会注意到所有模型都有尖锐的边缘。你已经要求艺术家使模型更平滑。
开发者这边还有其他什么可以做的来可能修复这些尖锐的三维模型边缘?
-
计算特定平滑角度值的法线。
-
通过 Unity 导入文件,而不是拖放文件。
-
将材质应用于每个三维模型。
-
确保场景中有灯光。
-
LateUpdate做什么?-
当帧过载时,替换标准的
Update函数。 -
LateUpdate运行资源较少,这使得它非常适合移动平台。 -
每个帧只调用一次更新。
LateUpdate每三帧调用一次。 -
LateUpdate是在渲染之前的执行顺序中的最后一个项目。
-
-
使用
GameObject.Find(如果有任何优点)的优点是什么?-
没有这些;它很慢且要求高。
-
如果不在每个帧上调用,它使编码对引用很有用。
-
GameObject.Find已弃用。 -
GameObject.Find在 Unity 项目之外的库资产数据中搜索。
-
-
我们是否必须在每个脚本中导入
UnityEngine库和MonoBehaviour?-
不,只要它们没有被应用到游戏对象上。
-
是的,或者 Unity 引擎会拒绝脚本。
-
是的,因为它们充当所有脚本的标题。
-
在所有情况下,必须继承
MonoBehaviour。
-
-
当从一个场景移动到另一个场景时,你会注意到第二个场景比之前使用相同艺术和灯光的场景要暗得多。
我们如何让场景中的灯光表现出应有的效果?
-
在第二个场景开启灯光之前,确保所有灯光都已关闭。
-
在进入第二个场景时,保持第一个场景中的所有灯光开启。
-
在加载时,将前一个场景中的灯光复制到新场景中。
-
在灯光设置中关闭自动生成,并手动生成灯光。
-
Debug.Log()的好处是什么?-
如果开发者想了解变量的值,这很有用。
-
它将字符串值发送到每个变量。
-
没有这些;它已弃用,所以我们不使用它。
-
它将信息记录到 Unity 的数据库中。
-
-
音频混音器对开发者有用吗?
-
不,它是专门为音频用户构建的;开发者使用 AudioSource。
-
是的,因为它可以用来在一点集中存储所有声音。
-
只有当开发者擅长单独处理音频时。
-
是的,它有助于音频的性能。
-
-
为什么一些开发者更喜欢 JSON 而不是 PlayerPrefs?
-
PlayerPrefs 在 JSON 之前发布,这使得它拥有更多的追随者。
-
JSON 可以与更多数据类型一起使用,并且是一个更兼容的 API。
-
两者都很好;这只是个人偏好的问题。
-
JSON 由 Unity 拥有,因此它集成了许多功能。
-
-
为什么我们会使用触发器框而不是碰撞体?
-
触发器和碰撞体执行相同的任务。
-
触发器比碰撞体功能更强大,且运行成本更低。
-
当另一个碰撞体/触发器进入它时,触发器可以调用代码。
-
触发器有不同的彩色框。
-
第十三章:第十三章:效果、测试、性能和替代控制。
在本章的最后一章中,我们将通过检查、支持、磨光和准备我们的游戏,使其构建并准备好在设备上播放,使其平台无关。因为我们的游戏将准备好在各种设备上播放,我们需要游戏支持尽可能多的屏幕比例。在第八章中,添加自定义字体和 UI,我们使游戏 UI 支持各种屏幕比例。然而,正如在第二章中讨论的,游戏是专门为 1,920 x 1,080 分辨率构建的。
在本章中,我们将使我们的游戏以不同的屏幕比例运行,以支持移动设备的使用。这涉及到更改 Unity 的 Canvas 缩放并更新我们的Player脚本控制,以更新其屏幕边界、触摸屏功能以及我们点击移动飞船的能力。此外,我们将使我们的游戏意识到它正在移动设备上播放,我们将进行一些更改,例如在商店中移除AD按钮,因为 PC 设备不支持广告。
“杀手波浪”的 PC 版本将应用更多磨光的特效,例如后处理,这将基本上使我们的游戏通过运动模糊、色差、色彩分级等效果变得更加美观。我们还将研究反射探针,以在level3场景中为我们的某些艺术资产创建镜像效果。
在本章中,我们将涵盖以下主题:
-
使用RigidBody应用物理。
-
为不同平台定制。
-
准备为移动设备构建“杀手波浪”。
-
应用 PC 视觉改进。
-
添加全局光照和其他设置。
-
构建和测试我们的游戏。
下一节将指定本章中将要涵盖的考试目标。
本章中将要涵盖的核心考试技能。
以下是在本章中将要涵盖的核心考试技能:
编程核心交互:
-
实现和配置游戏对象行为和物理。
-
实现和配置输入和控制。
-
实现和配置摄像机视图和移动。
在艺术管道中工作:
-
理解材质、纹理和着色器,并编写与 Unity 渲染 API 交互的脚本。
-
理解光照,并编写与 Unity 光照 API 交互的脚本。
针对性能和平台优化:
-
使用 Unity Profiler 等工具评估错误和性能问题。
-
识别针对特定构建平台和/或硬件配置的要求的优化。
在专业软件开发团队中工作:
-
展示对开发者测试及其对软件开发过程影响的了解,包括 Unity Profiler 和传统的调试和测试技术。
-
认识到构建模块化、可读性和可重用性脚本的技巧。
技术要求
本章的项目内容可以在 github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_13 找到。
您可以在 github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition 下载每个章节的项目文件。
本章的所有内容都包含在本章的 unitypackage 文件中。此文件包含一个 Complete 文件夹,其中包含我们在本章中将要完成的所有工作。因此,如果您在任何时候需要一些参考资料或额外指导,请查看它。
查看以下视频以查看 代码执行情况:https://bit.ly/3rYA6k4。
使用 RigidBody 应用物理
在本书中,我们使用了碰撞器和触发盒子来检测子弹或在我们商店的第一版中做出的选择。我们还提到了应用一个 level3 大 BOSS 游戏对象穿过它们。
以下图像显示了通过应用和调整 Rigidbody 组件,货物艺术资源被撞开,这是我们将在本节中实现的效果:
![图 13.1 – 由于 Unity 的 Rigidbody 组件,当被击中时盒子会飞起]

图 13.1 – 由于 Unity 的 Rigidbody 组件,当被击中时盒子会飞起
让我们从设置包含一些预制资源的 level3 场景开始:
-
在
Assets/Scene文件夹中。 -
双击
level3场景以打开它。 -
从
Assets/Prefab将physicsBarrier拖到 Hierarchy 窗口。 -
在所有轴上选择
physicsBarrier游戏对象。
以下截图显示了 level3 场景中的 physicsBarrier。注意绿色的轮廓,这表明这是我们的一系列盒子碰撞器,它们将包含我们的物理效果。
![图 13.2 – 线框是我们盒子的碰撞区域]

图 13.2 – 线框是我们盒子的碰撞区域
-
在
physicsBarrier中显示其三个子游戏对象。 -
选择这三个子对象,并将它们的 Rigidbody 组件设置为 Is Kinematic 被勾选。
-
以下截图显示了所有三个游戏对象被选中,并且正在更新 Rigidbody 设置:

图 13.3 – 所有 'physicsBarrier' 游戏对象子对象都有一个标记为 true 的 Rigidbody
是否运动学 将确保这三个游戏对象不受场景中物理的影响。即使我们勾选了 使用重力 复选框,游戏对象在场景开始时也不会像预期的那样开始下落。所以,无论场景中发生什么关于碰撞的事情,这三个游戏对象都将保持静止和坚固,以便它们能捕获所有物理引擎的反应。
更多信息
当选择了一个包含 刚体 组件的游戏对象时,以下属性将改变游戏对象在 Unity 物理引擎操作时的行为:
质量:游戏对象的千克质量(默认值:1)
阻力:空气阻力,其中零表示没有阻力(默认值:0)
角阻力:基于旋转的空气阻力(默认值:0.05)
关于 刚体 及其属性的更多信息,请参阅此处:docs.unity3d.com/ScriptReference/Rigidbody.html。
现在,我们可以将我们的货物盒子带入场景。
-
将
cargoBulk预制件从Assets /Prefab拖到physicsBarrier预制件中,确保 变换 属性值设置为默认值。 -
cargoBulk预制件应该就位,并看起来像以下截图所示:

图 13.4 – 盒子墙
为了加强 cargoBulk 以确保它在正确的时间倒塌,需要应用一个名为 TurnOnPhysics 的脚本。这将使所有 cargo 游戏对象在 38 秒后从 true 变为 false,(您可以随意打开 TurnOnPhysics 脚本,并在 Update 函数中将 38 调整为不同的数字)这是老板预计穿过货物盒子的时间。
physicsBarrier、cargoBulk 及其子游戏对象都被设置为碰撞体。目前,我们的老板被设置为当玩家射击时触发。然而,我们不想在这里将其设置为触发器,因为老板将像幽灵一样穿过货物盒子。
我们可以让 boss 以非触发模式启动,然后在关卡结束时,使用 时间轴 来开启它的触发器。要更改 是否触发 复选框,我们需要执行以下操作:
-
在 层次结构 窗口中选择 时间轴 游戏对象。
-
右键单击
boss时间轴 轨道资产,从下拉菜单中选择 在动画窗口中编辑。
以下截图显示了右键单击并加载 动画 窗口的位置:

图 13.5 – 加载动画窗口以更新 'boss' 游戏对象动画
我们在前一章中放置的 boss 游戏对象。我们可以在该窗口中添加两个关键帧来开启和关闭它,使用 是否触发 复选框。要执行此操作,请按照以下步骤进行:
-
将动画指示器拖动到动画窗口中动画轨道的开始处。
-
点击记录按钮。
-
在
boss游戏对象中,在检查器窗口的球体碰撞器组件中取消选择触发器。 -
回到
1193(这将是在boss游戏对象已经从盒子里爆出来的部分)。 -
在检查器窗口中选择触发器框,使其被勾选。
-
最后,回到动画窗口,按记录按钮关闭并关闭动画窗口。
-
保存场景并按
level3。
然而,完成此操作后,似乎有些不对劲。当我们到达关卡末尾时,盒子已经倒塌,当boss游戏对象与它们碰撞时,盒子看起来像是飘走了。这是因为我们场景中的游戏对象没有缩放到真实世界的大小,但重力是。为了让东西看起来更重,我们可以更改项目中的重力,如下所示:
- 在 Unity 编辑器顶部,转到编辑 | 项目设置 | 物理。
这里,我们有物理管理器,这是重力被设置为默认世界比例的地方。
-
将
-9.81更改为-1000。 -
另一个潜在问题可能是老板直接穿过货物盒子?为了解决这个问题,将
boss游戏对象的更新模式从正常更改为动画物理,并确保应用 根运动未勾选。最后,因为您的老板正在与盒子碰撞,请确保它有一个带有是运动学勾选的刚体。更多信息
您的项目重力也可以通过脚本进行更改,如下所示:
docs.unity3d.com/ScriptReference/Physics-gravity.html。 -
再次保存场景。现在,如果我们按下
boss游戏对象将爆裂穿过它们,如下面的截图所示:
![Figure 13.6 – The 'boss' game object colliding with the boxes
![img/Figure_13.06_B18381.jpg]
图 13.6 – boss游戏对象与盒子碰撞
更多信息
物理管理器包含项目中物理的全局设置。在物理管理器底部的许多有用设置之一是层碰撞矩阵。它包含了项目中可以和不能相互碰撞的所有层的名称。如果您想了解更多关于层碰撞矩阵的信息,请查看以下链接:docs.unity3d.com/Manual/LayerBasedCollision.html。
如果您对cargo游戏对象被boss游戏对象击中时的反应方式不满意,请调整其刚体属性值(包括在本节前面提到的那些)。
每个碰撞体都可以应用物理材质,这将影响物体的弹性和摩擦力。
创建和应用物理材质可以分三步完成:
-
在Project窗口中右键单击并选择Create | Physic Material。
-
选择New Physic Material并在Inspector窗口中更改其属性值(您也可以重命名文件,使其代表您试图实现的物理材料)。
-
选择一个带有碰撞器的游戏对象。然后,点击Material字段旁边的remote按钮并选择New Physic Material。

图 13.7 – 创建物理材料,更新其属性,并将材料添加到 Box Collider
更多信息
更多关于Physic Material的信息可以在docs.unity3d.com/Manual/class-PhysicMaterial.html找到。
我们的游戏现在已经应用了一些物理效果。现在,每次 Boss 穿过方块时,反应都会不同,而不是固定的动画。这是因为所有的移动都是基于 Unity 引擎的物理。
现在,让我们继续并使我们的游戏更适合多个平台。
为不同平台定制
在整本书中,我们一直在 Unity 编辑器中开发和玩游戏。在本节中,我们将开始考虑游戏在 Android 和 PC 版本之间的差异。例如,移动设备有触摸屏,所以如果我们的游戏能够检测到它在移动设备上运行,并因此实现正确的控制,这将是有用的。
此外,我们的游戏是以严格的 1,920 x 1,080 分辨率开发的;我们在商店场景的 UI 中引入了灵活性,并确保它能够适应各种宽高比。在本节中,我们将进一步使我们的游戏支持各种宽高比。
让我们开始修改Player脚本,使其支持触摸屏移动并在移动设备上发射。
使用触摸屏导航和发射玩家的飞船
在本节中,我们将重新审视Player脚本并添加一些功能,以便如果我们的游戏被移植到 Android 设备,玩家将具有触摸屏功能。
为了允许我们的玩家自动发射并导航到触摸位置,我们需要做以下事情:
- 在
Assets/Script文件夹中打开Player脚本。
在Player脚本顶部,我们将添加一些新的变量以支持新的控制系统。
-
将以下代码以及其余的变量添加到
Player脚本中:Vector3 direction; Rigidbody rb; public static bool mobile = false;
direction变量将保存玩家的触摸屏位置;rb将保存一个mobile变量,它只是一个static开关,让游戏的其他部分知道玩家的控制。
我们需要让游戏识别游戏正在运行的平台,以便它可以实现玩家的移动控制。Unity 有一个基于平台的编译,允许我们从一系列指令中选择,以便我们可以确定游戏正在运行的平台。
-
滚动到
Player脚本中的Start函数,并在Start函数的作用域内添加以下代码:mobile = false; #if UNITY_ANDROID && !UNITY_EDITOR mobile = true; InvokeRepeating("Attack",0,0.3f); rb = GetComponent<Rigidbody>(); #endif
在Start函数内,我们将我们的mobile bool变量设置为false。然后,我们运行一个平台定义的指令检查,以查看我们是否在运行 Android 设备且未使用 Unity 编辑器。
进一步信息
如果你想了解更多关于其他平台依赖编译的信息,请查看以下链接:
docs.unity3d.com/Manual/PlatformDependentCompilation.html。
如果我们使用的是 Android 设备,我们将进入这种特殊类型的if语句的作用域,并执行以下操作:
-
我们将
boolmobile变量设置为true。 -
使用 Unity 自带的
InvokeRepeating函数,使我们的Attack方法每0.3秒被调用一次,该函数充当自动射击工具。 -
分配
player_ship游戏对象的rb变量。 -
最后,我们关闭
if语句。
要使我们的InvokeRepeating方法在0.3秒内使用Attack方法发射子弹,我们需要修改Attack方法的if语句。
-
滚动到
Player脚本中的Attack方法,并将if语句替换为以下内容:if (Input.GetButtonDown("Fire1") || mobile)
通过将mobile变量添加到if语句的条件中,我们可以检查玩家是否按下了射击按钮,或者mobile bool变量是否设置为true。
现在,我们需要在Player脚本中的Update函数中添加更多功能,这包括两个我们尚未编写的新的方法,但将在以下代码块之后完成。
-
将
Player脚本中的当前Update函数及其内容替换为以下代码,以便它支持 PC 和移动控制:void Update () { if(Time.timeScale == 1) { PlayersSpeedWithCamera(); if (mobile) { MobileControls(); } else { Movement(); Attack(); } } }
我们更新的Update函数包含以下内容:
-
一个用于检查游戏是否被暂停的
if语句。如果游戏已被暂停,我们将跳过Update内容的其余部分。如果你想知道更多关于暂停游戏的信息,请查看第十章**,暂停游戏、更改声音和模拟测试。 -
在
if语句内,我们运行一个名为PlayersSpeedWithCamera的新方法,该方法将包含我们已编写的代码。我们只是将代码移动到方法中,以便它覆盖当相机应用速度时的 PC 和移动控制。 -
然后,我们有一个第二个
if语句,检查mobilebool变量是否设置为true或false。如果是true,我们将运行我们的MobileControls方法;否则,我们的 PCMovement和Attack方法将运行。 -
如前所述,我们有两个新的方法(
PlayersSpeedWithCamera和MobileControls)。第一个方法是将当前Movement方法中的代码简单复制粘贴,我们希望它适用于 PC 和移动控制。第二个方法将涵盖当玩家将手指放在屏幕上时,player_ship游戏对象移动到该位置时的触摸控制。
-
因此,让我们首先从
PlayersSpeedWithCamera方法开始。仍然在Player脚本中,向下滚动到Movement方法,选择并剪切第一个if语句。以下是我想要您剪切出的代码:if(camTravelSpeed > 1) { transform.position += Vector3.right * Time. deltaTime* camTravelSpeed; movingScreen += Time.deltaTime * camTravelSpeed; } -
然后,在
Player脚本中创建一个新的方法PlayersSpeedWithCamera,并将之前的if语句代码块粘贴到PlayersSpeedWithCamera方法的作用域内。
现在,PlayersSpeedWithCamera方法的内容将在移动和独立平台上运行。如果您想回顾一下相机移动速度的细节,请查看NavMesh, Timeline, 和模拟测试。
现在,让我们看看第二个方法MobileControls,它可以在Player脚本中找到。
-
在
Player脚本中编写以下方法,以便玩家可以在屏幕上导航player_ship:void MobileControls() { if (Input.touchCount > 0) { Touch touch = Input.GetTouch(0); Vector3 touchPosition = Camera.main. ScreenToWorldPoint(new Vector3(touch. position.x,touch.position.y,300)); touchPosition.z = 0; direction = (touchPosition - transform .position); rb.velocity = new Vector3(direction.x, direction.y,0)* 5; direction.x += movingScreen; if (touch.phase == TouchPhase.Ended) { rb.velocity = Vector3.zero; } } }
请记住,MobileControls方法在Update函数的每一帧都会被调用。在MobileControls方法内部,我们执行以下操作:
-
执行一个
if语句来检查设备屏幕上是否有超过一个触摸。如果手指触摸了屏幕,我们将进入if语句的作用域。 -
我们将触摸操作分配给
touch变量。更多信息
如果您想了解更多关于
Touch结构及其其他属性的信息,例如用于测量滑动手势的有用属性deltaPosition,请查看docs.unity3d.com/ScriptReference/Touch.html。 -
接下来,我们从 Unity 中提取一个现成的函数来转换屏幕的触摸位置,并将其存储在世界空间位置中。
更多信息
如果您想了解更多关于将点转换为世界空间的信息,请查看以下链接:
docs.unity3d.com/ScriptReference/Camera.ScreenToWorldPoint.html。 -
由于我们没有影响玩家飞船的z轴,我们将
touchPosition在 Z 轴上的值设置为 0。 -
存储触摸位置
Vector3位置减去玩家飞船的Vector3位置。 -
将
player_ship游戏对象发送到存储在direction中的Vector3位置。将其乘以5以使其移动得稍微快一些。 -
将
movingScreen变量中的值应用到directionx 位置。 -
最后,如果触摸阶段的状态已经结束(手指从屏幕上移开),将
rb velocity变量的值设置为 0。
因此,现在,玩家的飞船会自动发射并可以在屏幕上移动,这要归功于其Rigidbody组件。现在,我们需要确保当任何关卡结束时,我们停止玩家自动发射,并且Rigidbody不再影响玩家的移动。否则,当关卡结束时,玩家的飞船不会停止发射,存在无法从关卡中动画出来的风险。
为了防止玩家在关卡结束时持续射击和移动,我们需要做以下操作:
-
在
Assets/Script文件夹中打开ScenesManager脚本。 -
在
ScenesManager脚本内部,向下滚动到检查游戏是否结束的if语句(!gameEnding),并在其if语句内添加以下代码行:if (!gameEnding) { gameEndine = true; StartCoroutine(MusicVolume(MusicMode.fadeDown)); GameObject player = GameObject.Find("Player"); // ADD THIS CODE player.GetComponent<Rigidbody>().isKinematic = true; // ADD THIS CODE Player.mobile = false; // ADD THIS CODE CancelInvoke(); // ADD THIS CODE if (SceneManager.GetActiveScene().name != "level3")
在之前的代码块中,我们添加了四行新的代码,它们将执行以下操作:
-
从我们的
player_ship游戏对象缓存引用 -
访问
player_shiptrue -
将
mobileboolstatic变量设置为false -
运行 Unity 的
CancelInvoke函数来停止场景中正在运行的所有调用(停止自动发射)
- 保存
ScenesManager脚本。
现在,我们需要进入输入管理器并查看Fire1按钮。在这里,左鼠标按钮被设置为Alt Positive Button属性。为了在 Unity 编辑器中修复这个问题,请执行以下操作:
-
前往Edit | Project Settings | Input Manager。
-
设置
mouse 0。
我们的游戏现在能够意识到它将在哪种设备上运行,如果设备是在移动的 Android 设备上运行,则将实现触摸控制。
现在,让我们扩展我们游戏的支持范围,并确保我们的游戏覆盖各个平台上的各种屏幕纵横比和屏幕边界。
扩展屏幕纵横比支持
在本节中,我们将做两件事。第一件事是确保无论游戏运行在什么纵横比下,玩家都能飞来飞去。第二件事是确保文本 UI 不会受到不同屏幕纵横比的影响。
让我们从我们的第一个任务开始,使游戏在关卡期间支持多种屏幕纵横比。
在Assets/Script文件夹中打开Player脚本。然后,按照以下步骤操作:
-
在脚本顶部,变量所在的位置,注释掉宽度和高度浮点数;我们将用以下内容替换它们:
// float width; // float height; -
添加以下
GameObject数组来保存我们的新点:GameObject[] screenPoints = new GameObject[2];
我们刚刚添加的数组将保存两个点,以表示屏幕的边界。
-
接下来,在
Player脚本的Start函数中,我们需要注释掉以下内容:// height = 1/(Camera.main.WorldToViewportPoint(new Vector3(1,1,0)).y - .5f); // width = 1/(Camera.main.WorldToViewportPoint(new Vector3(1,1,0)).x - .5f); // movingScreen = width; -
添加以下方法名称:
CalculateBoundaries();
我们刚才输入的方法还不存在,所以现在让我们添加这个新方法。
-
仍然在
Player脚本中,添加以下方法和其内容以创建我们新的屏幕边界:void CalculateBoundaries() { screenPoints[0] = new GameObject("p1"); screenPoints[1] = new GameObject("p2"); Vector3 v1 = Camera.main.ViewportToWorldPoint (new Vector3(0, 1, 300)); Vector3 v2 = Camera.main.ViewportToWorldPoint (new Vector3(1, 0, 300)); screenPoints[0].transform.position = v1; screenPoints[1].transform.position = v2; screenPoints[0].transform.SetParent(this. transform.parent); screenPoints[1].transform.SetParent(this. transform.parent); movingScreen = screenPoints[1].transform. position.x; }
让我们通过CalculateBoundaries方法的步骤来了解它对游戏做了什么:
-
首先,它创建了两个新的游戏对象,并将它们命名为
"p1"和"p2"。 -
我们随后使用
ViewportToWorldPoint函数,这将为我们提供游戏世界空间位置以用于屏幕的边界。 -
然后,我们将我们的新
Vector3变量v1和v2应用于游戏对象位置数组 – 即"p1"和"p2"。 -
现在,
"p1"和"p2"代表边界,我们需要将它们作为Player脚本的子项,这将更新它们的变换 位置值。 -
最后,我们更新
movingScreenfloat值,以便在游戏有移动摄像机时使用我们的screenPoint值。
继续更新Player脚本,我们现在需要更新Movement方法的定向条件,以便它们支持我们新的游戏边界。
-
滚动到
Movement方法,并将所有四个旧的if语句替换为新的语句://OLD if (transform.localPosition.x < width + width/0.9f) //NEW if (transform.localPosition.x < (screenPoints[1].transform.localPosition.x - screenPoints[1].transform.localPosition.x/30f)+movingScreen) //OLD if (transform.localPosition.x > width + width/6) //NEW if (transform.localPosition.x > (screenPoints[0].transform.localPosition.x + screenPoints[0].transform.localPosition.x/30f)+movingScreen) //OLD if (transform.localPosition.y > -height/3f) //NEW if (transform.localPosition.y > (screenPoints[1].transform.localPosition.y - screenPoints[1].transform.localPosition.y/3f)) //OLD if (transform.localPosition.y < height/2.5f) //NEW if (transform.localPosition.y < (screenPoints[0].transform.localPosition.y - screenPoints[0].transform.localPosition.y/5f))
代码中上一行的每个新的if语句都将保持相同的目的,即从p1或p2游戏对象中获取值以获取屏幕边界的限制。这确保了玩家飞船不会飞出视野太远。
下面的截图显示了level1场景,其中p1和p2代表以不同于通常的 1,920 x 1,080 分辨率的新的游戏边界:

图 13.8 – 我们新的游戏边界
最后,我们需要更新我们的PlayerSpeedWithCamera方法,并将movingScreen变量设置为零,如果游戏摄像机没有向右移动。
-
在
Player脚本内部,转到PlayersSpeedWithCamera方法并添加以下else条件:else { movingScreen = 0; } -
保存
Player脚本。
现在,让我们继续并查看这个修复的第二部分。在这里,尽管游戏窗口现在支持各种宽高比,但一些图像和文本在视觉上可能难以吸引人。以下截图显示了如果我们更改典型的 1,920 x 1,080 分辨率,我们的游戏暂停界面会发生什么:

图 13.9 – 暂停界面在不同比例下的差异
如您所见,当文本和图像处于不同的宽高比时,它们会失去其比例。我们可以通过以下方式修复此问题:
-
在
Assets/Scene文件夹中双击level1场景。 -
在层次结构窗口中选择
Canvas游戏对象。然后,在检查器窗口中,将Canvas Scaler中的UI Scale Mode更改为Scale With Screen Size,如图下所示:

图 13.10 – 更新 Canvas Scaler | 缩放模式
- 更改
1920和1080。
现在,当我们的游戏窗口以各种屏幕尺寸显示时,将看起来更成比例。

图 13.11 – 在其他比例下,我们的暂停屏幕看起来更加统一
- 保存
level1场景并更新项目中所有场景的 Canvas Scaler。
通过这样,我们已经使我们的游戏在兼容性方面更加出色,它支持除标准 1,920 x 1,080 分辨率之外的各种宽高比。此外,我们的游戏控制能够感知游戏是在 PC 还是 Android 设备上运行。我们还使用了 Touch 结构来移动玩家在场景中的位置。
在下一节中,我们将在添加额外的效果和针对 PC 构建的通用润色之前,最终确定我们的移动游戏版本。
准备为移动设备构建“杀手波浪”
在本节中,我们将完成我们为 Android 开发的“杀手波浪”版本。在我们将游戏构建到 Android 之前,我们需要应用一些仅对 Android 构建必要的修复。
在本节中我们将应用以下修复:
-
调整照明以适应我们的 Android 设备
-
当按下暂停按钮时,确保我们的飞船不会移动到其位置
-
确保我们的游戏保持在横幅模式
-
当设备一段时间未被触摸时,停止屏幕变暗
-
将游戏纹理设置为较低的分辨率
-
为敌人和玩家添加预制爆炸效果
在应用这些小修复后,我们将为我们的 Android 设备构建游戏。
因此,让我们通过调整照明来开始我们的第一个任务。
为 Android 版本的“杀手波浪”设置照明
每个包含 3D 模型的场景都需要生成照明。Unity 编辑器的照明将与 Android 设备上提供的照明不同。
在应用当前默认照明设置后,以下截图显示了两个平台之间的差异。左边的图像是在 PC 上拍摄的,而右边的图像是从移动设备上拍摄的:
![Figure 13.12 – 平台之间照明的差异
![img/Figure_13.12_B18381.jpg]
图 13.12 – 平台之间照明的差异
因此,让我们调整照明,以便两个平台具有相似的亮度和对比度:
-
在 Unity 编辑器的顶部,转到窗口 | 照明 | 设置。
-
在照明窗口的顶部按下环境按钮,并应用以下值:
![Figure 13.13 – “照明”窗口属性的更新值
![img/Figure_13.13_B18381.jpg]
图 13.13 – “照明”窗口属性的更新值
- 接下来点击场景标签。在窗口顶部点击新建照明设置按钮。取消选中实时全局照明和烘焙全局照明。
当我们应用视觉改进时,我们将涵盖这两个设置,但到目前为止,实时全局照明会影响应用于其他对象的间接照明,以帮助创建更真实、柔和颜色的光。烘焙全局照明将灯光固定在 3D 资产上,以产生光照在表面上闪耀的外观,但我们的大多数灯光都在移动,所以这不会像烘焙光那样工作。
以下截图现在显示,PC 和移动端版本开始看起来相似:

图 13.14 – PC 与移动端光照现在看起来相似
-
现在,我们需要在
Assets/Material中启用并调整以下敌人材质的发射:-
basicEnemyShip_Inner:993600 (Hex), 和0.6 -
basicEnemyShip_Outer:4C0000(Hex), 和0.3 -
darkRed:801616(Hex), 和0.5
-
我们在第二章**,添加和管理对象中解释了如何更改材质的发射。更改这些值将给出以下输出:

图 13.15 – PC 与移动端;光照和颜色几乎相同
我们的游戏在任一平台上看起来都很漂亮且明亮。接下来,我们将修复移动端游戏暂停的小问题。
停止非自愿玩家控制
当在移动设备上玩游戏时,我们希望按下暂停按钮。但如果我们按下,游戏也会将按下视为移动命令,玩家的飞船将移动到按下位置的上左角。
因此,为了修复这个小问题,我们将在MobileControls方法中应用一个额外的条件,如下所示:
-
在
Assets/Script文件夹中打开Player脚本。 -
在
Player脚本内部,向下滚动到MobileControls方法,并将当前的if语句条件替换为以下内容:if (Input.touchCount > 0 && EventSystem.current.currentSelectedGameObject == null)
之前的代码块将运行一个检查,看看是否有手指触摸屏幕,就像之前一样,但也会检查在按下时该位置没有游戏对象。如果这些条件中的任何一个不满足,那么玩家将不会移动。
-
最后,为了导入
EventSystem,滚动到Player脚本顶部并添加以下命名空间:using UnityEngine.EventSystems; -
保存
Player脚本。
在下一节中,我们将进行一些最终的纹理优化,并应用一个现成的、应得的爆炸预制体。
杀手波浪的最终优化
在本节中,我们将通过减少纹理大小来对我们的游戏移动端进行一些优化。我们还将为敌人和玩家添加爆炸效果。
让我们先从减少纹理大小并压缩它们开始。
减少纹理大小和压缩
为了减小安装在 Android 设备上的.apk文件的大小,以及提高整体性能,我们可以通过 Unity 减小游戏纹理的大小,并应用压缩,这进一步降低了大小。
关键是减小纹理的大小,但不要过度;否则,纹理本身将会开始模糊,看起来很廉价。
在本节中,我们将减小以下纹理的大小:
-
PlayerShip及其附加组件(商店升级和推进器) -
我们两个级别中星星的背景壁纸纹理
-
商店按钮图标
让我们先选择并减小玩家飞船的纹理大小,并压缩它们:
-
在
Assets/Texture文件夹中。 -
选择以下所有文件名:
-
playerShip_diff -
playerShip_em -
playerShip_met -
playerShip_nrm -
playerShip_oc
-
-
所有这些文件都具有 512 x 512 的纹理大小,所以让我们将它们减小到 256 x 256,压缩它们,并通过将它们设置为以下检查器窗口截图中的值来关闭任何过滤:

图 13.16 – 将纹理大小从 512 x 512 减小到 256 x 256
-
对以下纹理做同样的处理,所有这些纹理都可以放在同一个文件夹中。然而,这次,将纹理大小从 1,024 x 1,024 全部降低到 64 x 64:
-
b. Shot_diff -
b. Shot_nrm -
c. Bomb_diff -
c. Bomb_nrm
-
继续对其他纹理做同样的处理,并通过在shop和level1场景之间进行游戏来查看游戏中的结果。请自行决定是否这样做。
更多信息
如果你想了解更多关于导入到项目中的纹理以及如何调整它们的质量级别,请查看以下链接:docs.unity3d.com/Manual/ImportingTextures.html。
现在,让我们继续前进,通过一些小的脚本调整,为我们的每个玩家和敌人添加一个现成的粒子爆炸效果。
为我们的玩家和敌人添加爆炸效果
现在是时候给我们的游戏对象添加一个预制爆炸效果,以表示它们在被射击时的破坏以及它们对 Boss 的一般影响。我们之前在第四章中介绍了粒子系统,应用艺术、动画和粒子。在这里,我们将应用一些脚本,以便当调用Die方法时,我们将实例化我们的explode预制体。
为了在敌人死亡时实例化爆炸预制体,我们需要做以下操作:
-
在
Assets/Script文件夹中打开EnemyWave脚本。 -
在
Die方法内部,将其内容替换为以下内容以实例化explode游戏对象:GameObject explode = GameObject.Instantiate(Resources.Load("explode")) as GameObject; explode.transform.position = this.gameObject.transform.position; Destroy(this.gameObject);
在前面的代码块中,我们在当前的 Destroy 函数上方添加了两行额外的代码。我们已经在 第二章,添加和操作对象 中详细介绍了这一点。这两行额外的代码执行以下操作:
-
当
Die方法运行时,它将从Assets/Prefab创建explode预制件。 -
explode实例的位置更新为与敌人相同的地点。
- 保存
EnemyWave脚本,并重复此过程对EnemyFlee和BossScript脚本进行操作。
最后,对于我们的 Player,我们将添加类似的东西,但也会为 player_ship 被销毁时添加一个延迟,这样我们就可以在重新加载场景之前看到爆炸效果。
-
仍然在同一
Player脚本中,向下滚动到Die方法,并用以下内容替换其内容:GameObject explode = GameObject.Instantiate(Resources.Load("Prefab/explode")) as GameObject; explode.transform.position = this.gameObject.transform.position; GameManager.Instance.LifeLost(); Destroy(this.gameObject);
在前面的代码中,我们已经更新了玩家的 Die 方法,使其创建一个预制件爆炸,并将其位置放在玩家位置处。
然而,我们需要在 GameManager 脚本中添加一个延迟,在之前代码块引入的地方。
-
在继续
GameManager脚本之前,保存Player脚本。 -
打开
GameManager脚本,以便在场景更新时添加延迟。 -
在
GameManager脚本中,向下滚动到LifeLost方法,选择其内容,剪切它(剪切,而不是 删除,因为我们将要将其粘贴到其他地方),并用以下代码替换LifeLost方法:StartCoroutine(DelayedLifeLost());
这里,我们正在延迟 LifeLost 方法的内容。然而,在这里,我们将使用 StartCoroutine 来创建延迟,如前一行代码所示。
-
接下来,我们将把原始
LifeLost方法的内容粘贴到下面的代码块中:IEnumerator DelayedLifeLost() { yield return new WaitForSeconds(2); // PASTE LIFELOST CONTENT HERE }
在前面的代码块中,我们添加了 IEnumerator。这将通过 StartCoroutine 执行,并等待 2 秒。如果你的 IEnumerator 在 IDE 中有下划线错误,请在脚本顶部添加库 using System.Collections。
将之前剪切的 LifeLost 内容粘贴进来,然后保存 GameManager 脚本。
以下截图显示了应用了粒子爆炸的游戏对象:

图 13.17 – 粒子效果
现在,是时候创建安卓平台的构建了。
设置安卓的构建设置
在本节中,我们将设置我们的玩家设置并构建适用于安卓设备的 Unity 项目。为了测试目的,我将使用一款相当旧的平板电脑和一部最新的手机来查看这两款设备在设置方面是否存在任何差异。
在设置我们的玩家设置之前,请确保您已安装 Java 开发工具包和安卓软件开发工具包。为此,请执行以下操作:
-
在 Unity 编辑器的顶部,转到 Edit | Preferences。
-
然后,在 Unity Preferences 窗口中点击 External Tools。
-
以下截图显示了这两个开发工具包,以及它们的下载按钮。如果你没有其中任何一个,可以通过 Unity Hub 安装,我们已在第一章**,设置和构建我们的项目中介绍过。

图 13.18 – 两个开发工具包都已安装
更多信息
如果你需要有关开发工具包安装过程的更多具体信息,请查看以下链接:docs.unity3d.com/Manual/Preferences.html。
现在,让我们继续到玩家设置并设置我们的游戏:
-
在 Unity 编辑器的顶部,转到文件 | 构建设置…。
-
确保所有场景都已设置在构建中的场景中。
-
从平台列表中选择Android,然后点击切换平台。

图 13.19 – 在构建设置中,选择“Android”然后选择“切换平台”
-
点击玩家设置...以进入 Android 设置的下一阶段。
-
在检查器窗口的顶部,更新公司名称和产品名称字段为你想要的任何内容。
-
选择分辨率和显示选项卡,取消选中纵向和纵向颠倒。
-
选择其他设置选项卡。
-
滚动到标识部分。在以下截图中,你会看到包名字段已填写我们的公司名称和产品名称(com.Packt.KillerWave):

图 13.20 – 在玩家设置的“标识”部分添加我们的包名
-
此外,如果你的设备可以处理,请将最小 API 级别设置为23或更高。如果不行,当我们进行构建时,你将在控制台窗口中收到有关更改最小 API 级别值的错误。
-
返回到构建设置窗口,并点击构建按钮。
-
你将被要求为
apk命名和指定位置。选择你想要的位置和文件名,然后点击保存。小贴士
如果你遇到Gradle 构建失败错误,尝试在构建设置窗口中将构建系统更改为内部。
-
最后,确保你的 Android 设备处于 USB 调试模式,并将
apk复制到设备上。 -
前往设备上已复制
apk的位置,选择它以安装和运行。小贴士
在 Android 设备上测试游戏时,你可能会发现当屏幕未被触摸时,设备的亮度变暗可能会分散你的注意力。
我们可以通过添加以下代码来修复这个问题,最好是在
GameManager脚本的Awake函数中,因为这关系到游戏的整体交互:#if UNITY_ANDROIDScreen.sleepTimeout = SleepTimeout.NeverSleep;#endif
这就带我们完成了为移动设备构建游戏的过程。在本节中,我们涵盖了设置我们的光照设置,以确保它们与我们在 Unity 编辑器中看到的一致。之后,我们清理了一些小问题,以免我们在设备上按下暂停按钮时不小心将玩家飞船移动到暂停按钮的位置。我们还通过减小游戏纹理的大小来减小我们的apk大小。这也帮助了 Android 设备在玩游戏时的性能。然后,我们添加了我们的explode预制体,并对我们的脚本进行了一些修改,以确保在正确的时间和地点实例化爆炸。
最后,我们完成了设置我们的 Unity 构建文件的过程,并将其复制到 Android 设备上,以便安装和运行。
如果您已经走到这一步,构建了游戏,并且一切如预期般工作,那么恭喜您!如果没有,或者您在过程中遇到了一些问题,请不要担心——其他 Unity 用户可能遇到过类似的问题,通过一些谷歌搜索就能找到它们的解决方案。现在,我们将开始对游戏进行故障测试。

图 13.21 – 在旧平板上运行的 Killer Wave
在下一节中,我们将为我们的 PC 版本添加光泽和光泽。
应用 PC 视觉改进
在本节中,我们将专注于 PC 版本,我们将有更多的空间来应用效果,因为很可能玩这款游戏的 PC 将比移动设备更强大。
我们将涵盖诸如后处理等内容,通过它我们可以创建一些漂亮的效果,使我们的游戏更加闪耀。我们可以通过应用如模糊运动、模糊屏幕边缘、弯曲屏幕以产生穹顶屏幕效果以及改变色彩来实现这一点。
我们还将查看光照和反射,以便我们有一个稍微修改过的商店场景,它将包含多个光源并使游戏更加突出。在level3场景中,我们将添加反射资产以展示这些反射探针在我们艺术资产中的应用。
让我们先来讨论一下后处理。
后处理
在本节中,我们将安装并应用后处理效果到我们的游戏中。这将为我们提供在电影中使用的效果,例如胶片颗粒、色差、色彩分级和镜头畸变。让我们从将此包安装到我们的项目中开始吧。
安装后处理
后处理通过包管理器直接安装到我们的项目中。在前一章中,我们以类似的方式安装了默认可播放内容。这次,我们不会去资产商店;我们可以从包:Unity 注册表下载并安装后处理。
以下步骤将指导您在项目中安装后处理的过程:
-
在 Unity 编辑器的顶部,点击窗口 | 包管理器。
-
在包管理器窗口的左上角,将其下拉菜单更改为包:Unity 注册表。
-
将显示一个长长的包列表;从列表中,你可以向下滚动并选择将其中的
Post Processing选中。 -
最后,在右下角点击安装。
-
以下截图显示了已选择Post Processing的包管理器:
![Figure 13.22 – The Package Manager with Post Processing selected to install
![img/Figure_13.22_B18381.jpg]
图 13.22 – 选择带有后处理的包管理器以安装
-
在 Unity 编辑器的顶部,转到文件 | 构建设置...。
-
选择PC, Mac & Linux 独立版,然后选择切换平台。
我们的项目现在已安装了后处理。有了这个,我们可以开始为我们的独立游戏准备一些场景。
准备并应用后处理到我们的标题和关卡场景
在本节中,我们将对我们的title场景进行一些更改,以便它支持我们的图像和文本受到后处理的影响。本节结束时,我们的title场景将看起来更加引人注目,如下面的截图所示:
![Figure 13.23 – Difference between post-processing and not
![img/Figure_13.23_B18381.jpg]
图 13.23 – 后处理与未后处理的区别
要将后处理应用到我们的title场景,我们需要执行以下操作:
- 在
Assets/Scene中打开title。
我们现在需要更改Canvas游戏对象的一些属性值,以便后处理更改来自摄像机的输入,而不仅仅是Canvas本身。
-
在
Canvas游戏对象中。 -
在Canvas组件属性的渲染模式选项中选择屏幕空间 - 相机。
-
将Main Camera从层次结构窗口拖动到渲染相机属性字段。
接下来,我们将向我们的Main Camera游戏对象添加两个后处理组件。
-
在层次结构窗口中选择主相机。
-
在检查器窗口中点击添加组件按钮。
-
在下拉列表中输入
Post Process Layer。当你在列表中看到它的名字时,选择它。 -
在
PostProcessing中,将Everything更改为PostProcessing以移除Post Processing Layer组件中的警告信息。 -
点击
Post Process Volume直到它在列表中可见。然后,选择它。 -
在Post Process Volume组件的顶部,勾选全局框。
-
在
Assets/Scene文件夹中,在检查器窗口的Profile参数中。 -
Game窗口将应用Profile后处理效果,这可能对你来说可能过于极端。我们可以将权重从1调整到0。我将我的设置为0.6。
以下截图显示了我们的标题场景Game窗口,以及之前步骤中提到的两个Post Process Layer和Post Process Volume组件以及突出显示的区域,供参考:

图 13.24 – 属性值已更改的后处理组件(在主相机游戏对象上)](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/unt-cert-prog-exgd-2e/img/Figure_13.24_B18381.jpg)
-
保存场景。
-
对
shop场景重复步骤 2–12,但不是在应用后处理之前和之后(将权重属性设置为1),如果你的相机背景没有设置为黑色,现在将其设置为黑色:

图 13.25 – 应用和未应用后处理效果的我们的商店场景
- 现在,重复步骤 2–13为
gameOver场景。最终结果应类似于以下内容:

图 13.26 – 应用后处理效果的游戏结束屏幕
- 重复步骤 5–13,但不是应用文本后处理配置文件,而是添加默认后处理配置文件。
以下图像显示了应用和未应用默认后处理配置文件的level3场景示例:

图 13.27 – 未应用和已应用后处理的游戏屏幕
这就是我们需要实现后处理配置文件的所有场景。在下一节中,我们将简要介绍我们可以应用的所有效果。
后处理效果(覆盖)
在本节中,我们将简要讨论后处理包为我们提供的功能。到本节结束时,你将更加熟悉这些效果,并能够创建自己的后处理配置文件。
现在我们已经看到了后处理对我们游戏的影响,我们可以讨论每个效果。让我们先加载title场景并更改我们已有的内容:
-
在
Assets/Scene文件夹中加载title场景。 -
在层次窗口中选择主相机。
本节的主要关注点将是后处理体积组件中的覆盖部分:

图 13.28 – 后处理体积组件中的‘Bloom’,‘色差’和‘色彩分级’位置
因此,让我们查看检查器窗口中的一些后处理体积的覆盖设置。然后,我将提供一个链接,我鼓励你探索,以便你可以玩一些值。
Bloom
此效果在图像中明亮区域的边缘创建光晕。
我们可以通过选择bloom复选框左侧的箭头(如下面的屏幕截图右上角所示)来扩展内容:

图 13.29 – 设置 bloom 属性值
在前面的屏幕截图中,我们已经打开了所有属性。只需取消选择并选择每个属性,就可以看到(如果有的话)对每个属性的影响。还可以尝试更改一些值。如果你觉得效果过强,可以使用前面的屏幕截图作为备选方案。
在这里值得一看的有趣属性是 1.14,bloom 效果将会增强。然而,如果我们把值设置得太低,我们可能会过度处理并破坏我们游戏的外观,如下所示:

图 13.30 – '阈值' 属性级别
希望我已经让你足够好奇,继续探索和实验 bloom 效果。
更多信息
关于bloom效果的更多信息可以在docs.unity3d.com/Packages/com.unity.postprocessing@2.1/manual/Bloom.html找到。
接下来,我们将探讨色差。
色差
这种效果模仿了当现实世界中的相机镜头无法将所有颜色在相同点汇聚时产生的效果。
下面的屏幕截图显示了我们的当前设置:

图 13.31 – 设置色差属性值
这种效果在游戏窗口的边缘更为明显。例如,在下面的屏幕截图中,我已经将图像和其文本向上移动,以便我们可以更明显地看到这两个组件开始变形:

图 13.32 – '色差' – 关闭与开启
更多信息
关于色差效果的更多信息可以在docs.unity3d.com/Packages/com.unity.postprocessing@2.1/manual/Chromatic-Aberration.html找到。
接下来,我们将探讨我们应用于 title 场景的最终效果 – 色彩分级。
色彩分级
这种效果改变了 Unity 产生的最终图像的颜色和亮度。色彩分级在所有后处理效果中具有最广泛的属性范围。我已经将这些属性分成几个项目符号段:
- 模式:

图 13.33 – 设置 '色彩分级' 覆盖值
在这里,我们有三种颜色评分模式可供选择,以便我们可以改变相机的最终图像。在前面的屏幕截图中,Unity 提醒我们更改色彩空间从伽玛到线性。如果您想这样做,可以在编辑 | 项目设置 | 玩家 | 玩家设置 | 其他设置中进行更改。
- 色调映射:

图 13.34 – 色调映射
这为我们提供了在颜色评分过程结束时可以使用的色调映射算法的选择。
- 白平衡:

图 13.35 – 白平衡
这会改变最终图片的温度和色调。
- 色调:

图 13.36 – 色调
在这里,您可以调整饱和度、对比度、色调偏移、颜色滤镜和曝光后(EV)选项,这些选项与光晕效果的阈值属性类似,很容易过度处理并提供一些非常明亮或暗淡的结果。
- 频道混合器:

图 13.37 – 频道混合器
这会改变每个整体图像的 RGB 通道。
- 轨迹球:

图 13.38 – 轨迹球
在这里,三个轨迹球(提升调整暗色调,伽玛调整中间色调,增益调整高光)影响最终图像的整体色调。
- 评分曲线:

图 13.39 – 评分曲线
评分曲线是调整最终图像中特定色调、饱和度或亮度的范围的高级方法。
更多信息
更多关于颜色评分效果的信息可以在docs.unity3d.com/Packages/com.unity.postprocessing@2.1/manual/Color-Grading.html找到。
这就结束了我们对标题场景所有三个后期处理覆盖效果的探讨。如果您想了解更多关于其他可用效果的信息,请查看以下链接,您可以阅读有关可以应用于 Unity 场景的其他 11 个效果的说明:docs.unity3d.com/Packages/com.unity.postprocessing@2.1/manual/index.html.
抗锯齿模式
在本节中,我们将查看后期处理层组件中不同类型的抗锯齿。如您所知,抗锯齿平滑游戏对象粗糙的边缘,以消除阶梯效应。Unity 提供了三种不同的算法来平滑边缘。
以下模式可供选择:
-
快速近似抗锯齿(FXAA):由于其快速算法,这通常用于移动平台。这是最有效的方法,但不支持运动矢量。
-
亚像素形态抗锯齿 (SMAA):这是高质量但系统性能要求更高的技术。
-
时间抗锯齿 (TAA):这是一种高级且要求较高的技术,它使用运动矢量(运动矢量是运动估计过程中的关键元素)。
以下截图显示了应用了不同抗锯齿技术的玩家飞船:
![Figure 13.40 – 使用抗锯齿模式的视觉差异]
![img/Figure_13.40_B18381.jpg]
图 13.40 – 使用抗锯齿模式的视觉差异
如您所见,抗锯齿的目的是去除锯齿边缘,但鉴于我们的游戏背景大多是深色,这些边缘并不那么明显。
更多信息
如果您想应用抗锯齿并想了解更多信息,请查看以下链接:docs.unity3d.com/Manual/PostProcessing-Antialiasing.html。
接下来,我们将探讨创建和应用我们自己的后处理配置文件,这些配置文件是在 应用 PC 视觉改进 部分的开头创建的。
创建和应用后处理配置文件
在后处理的最后部分,我们将讨论创建后处理配置文件。从那里,您可以选择(如果您想的话——我鼓励您这样做)创建自己的配置文件并将其应用到 检查器 窗口中的 后处理体积 组件。最后,您将能够添加/删除自己的效果以改变独立游戏的最终外观。
因此,为了创建和添加我们自己的效果,我建议我们回到一个我们已经准备好的场景——title 场景:
-
在
Assets/Scene中打开title场景。 -
在 层次 窗口中选择 主相机 游戏对象。
-
点击 后处理体积 组件内部的 新建 按钮(如图所示):
![Figure 13.41 – 创建新的后处理配置文件]
![img/Figure_13.41_B18381.jpg]
图 13.41 – 创建新的后处理配置文件
-
要添加自己的后处理效果,请点击 添加效果... 按钮(如图所示),位于 后处理体积 组件的底部,并从下拉列表中选择一个效果。
-
应用效果后,点击 全部 以启用所有属性(如图所示):
![Figure 13.42 – 启用所有属性]
![img/Figure_13.42_B18381.jpg]
图 13.42 – 启用所有属性
-
如果您想删除效果,请点击效果右上角(在 关闭 按钮上方)并从下拉列表中选择 删除(如图所示)。
-
如此简单!如果您想查看文件的位置,请点击 PostProcessProfile 字段,如图所示:
![Figure 13.43 – 应用和定位后处理配置文件]
![img/Figure_13.43_B18381.jpg]
图 13.43 – 应用和定位后处理配置文件
-
在项目窗口中,位置将闪烁黄色(如前一张截图所示),这也是你可以重命名文件以反映配置文件用途的地方(右键单击文件,从下拉菜单中选择重命名)。
-
如果你不喜欢你所创建的,你可以从项目窗口中删除后处理配置文件文件,然后点击后处理体积中配置文件参数右侧的小远程按钮,再次添加TEXT配置文件(或你想要的任何配置文件)。
这是对 Unity 提供的后处理包的全面概述。在本节中,我们导入了我们的 Unity 包并为每个游戏场景添加了后处理组件。从那里,我们应用了现成的配置文件来定制场景的后处理效果。然后,我们简要回顾了一些可以添加到后处理体积组件中的效果,这些效果已经存在于我们的场景中。
我们通过改变场景的抗锯齿属性来结束本节。这样,我们消除了艺术资产的粗糙边缘。
我鼓励你创建自己的配置文件,但如果你觉得你需要更多配置文件来尝试,你可以以小额价格从资源商店购买配置文件的集合。
在下一节中,我们将查看照明设置并应用一些全局照明、照明和雾效到我们的商店场景中。
添加全局照明和其他设置
在本节中,我们将通过添加来自level3场景的艺术资产和添加红色发射材料来为我们的商店场景添加背景。
Unity 目前正在开发一个新的全局照明照明包,这意味着我们正在使用的这个版本最终将被淘汰。好消息是,它将在 2020 LTS 期间得到支持。
我们将激活场景的实时全局照明,红色发射材料将在走廊的表面上发光。我们还将为商店展示和玩家飞船添加额外的灯光,使其更加突出。最后,我们将添加一些黑色雾气,以在发光的灯光周围创造一些黑暗。
以下截图显示了完成本节前后商店场景的比较:
![Figure 13.44 – Global illumination/Lighting/Fog – off versus on]
![Figure 13.44_B18381.jpg]
图 13.44 – 全局照明/照明/雾 – 关闭与开启
因此,让我们从这个部分开始,添加我们将用于商店场景的艺术资产。
将艺术资产添加到我们的商店场景中
在本节中,我们将拖放一些预制的艺术资产到我们的shop场景中。从那里,我们可以继续设置我们的照明设置。
要将艺术资产应用到我们的shop场景中,我们需要执行以下操作:
-
首先,从
Assets/Scene加载shop场景本身。然后,双击shop场景。 -
在
Assets/Prefab文件夹中。 -
将环境预制体拖放到层次结构窗口中。
从游戏窗口查看场景时,将如下所示:

图 13.45 – 我们商店场景的当前外观
我们引入到shop场景中的艺术资产应标记为静态。具体来说,需要标记贡献全局光照,这样我们才能从我们的红色发射带生成所需的灯光,如图中所示的前一个屏幕截图。
- 以下屏幕截图显示了标记为静态的环境游戏对象(及其所有子对象)的检查器窗口:

图 13.46 – ‘环境’游戏对象及其标记为静态的子对象
更多信息
如果场景中存在我们希望受场景光照影响的移动游戏对象,我们需要添加光照探针来更新该移动游戏对象上的任何间接颜色。
如果你想了解更多关于光照探针的信息,请查看docs.unity3d.com/Manual/LightProbes.html。
现在,我们需要禁用场景中当前任何类型的灯光,以免稀释我们试图实现的效果:
- 如果层次结构窗口包含方向光,请选择它,然后在键盘上按删除键。
现在,我们可以设置光照设置,以便它们支持实时全局光照。为此,我们需要访问光照窗口并输入一些值。
-
如果光照窗口尚未加载,在 Unity 编辑器中,屏幕顶部,选择窗口 | 渲染 | 光照。
-
在光照窗口中选择场景按钮。现在,让我们开始关闭所有环境灯光。将您的环境设置调整为以下屏幕截图所示:

图 13.47 – ‘光照’窗口及其在‘场景’标签页中的更新属性值
- 如前一个屏幕截图所示,我们已经移除了场景可能拥有的任何类型的灯光。现在,我们可以回到场景标签页,并打开位于光照窗口环境部分下方的实时全局光照,如果所有设置都变为灰色,我们需要在场景标签页右上角创建新的光照设置。

图 13.48 – 打勾‘实时全局光照’
- 确保取消勾选烘焙全局光照,因为我们不希望我们的灯光在运行时计算。这是因为它会消耗 RAM 和 HDD/SSD 空间。
在照明窗口的设置中,我们可以降低一些光照贴图设置的值,以便地图不那么详细,并且在较慢的系统上生成光照也更快。
- 将光照贴图设置保留在其默认值,如下面的截图所示:

图 13.49 – 将“光照贴图设置”属性值更新为这里显示的值
-
在照明窗口的底部,确保自动生成未被勾选,并点击生成照明按钮。
-
等待 Unity 编辑器右下角的蓝色条完成并消失。
我们将在游戏窗口中看到以下输出:

图 13.50 – 我们的“游戏”窗口显示我们的商店场景在强烈的光线下变得苍白
更多信息
我们可以(如果想要的话)通过选择场景标签下的着色按钮,并从下拉菜单中选择间接照明来检查我们从当前的照明设置中创建的间接照明(完成后不要忘记将其改回着色)。
我们的shop场景看起来过于明亮且红色过重,淹没了场景。然而,我们几乎得到了我们想要的效果。现在,我们可以从照明窗口打开一些雾,以创建一个黑暗的巷子,红色辐射从中渗透出来。
- 要添加
shop场景,在我们的照明窗口中,靠近底部,我们需要应用以下值:

图 13.51 – “雾”设置属性值
如以下截图所示,我们的shop场景:

图 13.52 – 带有黑色雾背景的“游戏”窗口
- 最后一步是将
shopLights预制件从Assets/Prefab拖放到层次结构窗口中,以照亮玩家飞船:

图 13.53 – 添加到玩家飞船上的小光源
- 保存
shop场景。
通过这种方式,我们已经成功从我们的shop场景中移除了默认照明并应用了shop场景的背景。
在下一节中,我们将讨论和实现level3场景的一个小部分,这样我们就可以开始添加具有反射的艺术资产。
反射探针
在本节中,我们将介绍我们游戏的最终艺术资产。该资产将反映场景中的环境,如下面的截图中的两个球体雕像所示:

图 13.54 – 背景中的反射球体
您可以想象,拥有一个像镜子一样反射周围环境的材质是多么有用。我们将添加level3场景并校准其属性值,以获得令人满意的结果,而不会影响我们的系统资源。
因此,让我们首先加载level3场景:
- 在
Assets/Scene文件夹中双击level3场景。
现在,我们将我们的shinySphere资产放置到场景中。
-
在
Assets/Prefab文件夹中。 -
将shinySphere拖入层次结构窗口。
-
在层次结构窗口中选择shinySphere资产,并确保其变换值设置如下所示:
![Figure 13.55 – ‘shinySphere’变换属性值
![img/Figure_13.55_B18381.jpg]
图 13.55 – ‘shinySphere’变换属性值
shinySphere游戏对象现在应该位于关卡末尾的货物块旁边的位置,如下截图所示:
![Figure 13.56 – ‘shinySphere’游戏对象的放置
![img/Figure_13.56_B18381.jpg]
图 13.56 – ‘shinySphere’游戏对象的放置
在我们添加第二个shinySphere游戏对象之前,让我们向此游戏对象添加一个反射探针组件,如下所示:
-
在层次结构窗口中,展开shinySphere游戏对象并选择spheres子游戏对象。
-
在层次结构窗口中右键单击spheres游戏对象,然后选择灯光|反射探针。
-
球体游戏对象现在有一个名为反射探针的子游戏对象。选择此游戏对象。
-
在检查器窗口中,我们有反射探针组件及其值。首先,让我们将类型值更改为以下截图所示的值,以便我们的游戏对象能够反射其环境:
![Figure 13.57 – 将“类型”值更新为这里显示的值以创建反射
![img/Figure_13.57_B18381.jpg]
图 13.57 – 将“类型”值更新为这里显示的值以创建反射
我们的shinySphere游戏对象现在将每帧更新其反射。
- 接下来,我们将修改运行时设置的值以提高反射的准确性。如果盒子投影变灰,请在构建设置中从Android切换到PC、Mac & Linux 独立构建。使用以下截图所示的值:
![Figure 13.58 – 在“运行时设置”中提高反射的准确性
![img/Figure_13.58_B18381.jpg]
图 13.58 – 在“运行时设置”中提高反射的准确性
更多信息
盒子投影将有助于提高环境中提供的反射的准确性。如果您想了解更多信息,请查看以下链接:docs.unity3d.com/Manual/AdvancedRefProbe.html。
- 最后要更新的属性值可以在Cubemap 捕获设置中找到。更改这些值将改变最终反射的外观(简单地估计背景属性应该是什么颜色,以便它适合你的场景):

图 13.59 – 将‘Cubemap 捕获设置’属性值更新为这里显示的值
如果使用不当,反射探针可能会根据游戏指向的平台造成性能问题。例如,使用之前的设置,更高的分辨率将显示更清晰的反射,但显然需要更多的资源。
更多信息
更多关于反射探针及其性能的信息,请查看以下链接:docs.unity3d.com/Manual/RefProbePerformance.html。
要复制level3场景,我们需要做以下几步:
-
在层次结构窗口中选择shinySphere游戏对象,并在检查器窗口的右上角点击覆盖 | 应用全部来更新预制体。
-
要复制并粘贴shinySphere游戏对象,在层次结构窗口中右键点击shinySphere,从下拉列表中选择复制。
-
在层次结构窗口中(在空白区域,靠近底部)右键点击,并从下拉列表中选择粘贴。
-
最后,将shinySphere游戏对象移动到x轴的右侧。
-
保存场景。
以下截图显示了两个shinySphere游戏对象反射的环境:

图 13.60 – 两个带有反射的‘shinySphere’游戏对象
如果在未来的任何其他 Unity 项目中,你需要创建一个闪亮的表面、大理石地板、全新的闪亮汽车等等,使用反射探针将满足这些需求。
这样,我们已经到达了游戏完成的点,我们已经涵盖了游戏设计文档中指定的所有内容。现在是一个很好的时机来构建我们游戏的独立版本,看看它的运行情况如何。有没有什么错误?我们打算如何测试我们的游戏?让我们继续前进,看看我们如何解决这些问题。
构建和测试我们的游戏
我们已经到达了可以构建和运行我们的游戏的点,而不仅仅是测试 Unity 编辑器中的游戏场景。本节不仅将涉及构建游戏,就像我们之前为游戏的 Android 版本所做的那样,还将查看我们的最终构建是否有任何错误。我们还将通过使用性能分析器中的性能峰值来查找沿途的任何潜在问题。
让我们先构建我们的游戏,看看它在测试之前运行得如何。
要为 PC 构建我们的游戏,我们需要做以下几步:
-
在 Unity 编辑器的顶部,转到文件 | 构建设置...
-
确保所有场景都在构建中的场景列表中,并且顺序正确。
-
平台应设置为PC、Mac & Linux 独立版。如果不是,请选择它并选择切换平台按钮。
接下来,我们需要在玩家设置...窗口中添加这个游戏打算使用的纵横比:
-
选择玩家选项卡。
-
在检查器窗口中,展开分辨率和展示内容。
-
展开支持的纵横比内容。
取消选择以下截图显示的纵横比:

图 13.61 – 选择 16:10 和 16:9 的纵横比(典型宽屏比例)
- 返回到构建设置窗口,按构建按钮。
以下截图显示了这些引用被突出显示:

图 13.62 – 所有游戏场景已添加并排序(构建中的场景),在“平台”设置中选择了‘PC、Mac & Linux 独立版’,并点击了“构建”
-
在出现的Windows 资源管理器窗口中,选择你希望安装游戏的地点并点击保存按钮。
-
一旦游戏构建完成,运行其
.exe文件。
现在,让我们修复我们游戏中可能出现的任何潜在问题。
解决错误
假设我们已经将我们的游戏发送出去进行错误测试,并收到了几个错误测试人员对错误、游戏 UI 和游戏性能的反馈。
以下几节包含四个报告,我希望你阅读并思考。我们将在本章末尾讨论答案。
让我们从第一个错误报告开始。
错误报告 – 独立 AD 按钮
已有报告称,当我们的错误测试人员玩 PC 版本的游戏时,他们无法在商店场景中观看广告。
我们如何解决这个问题?
以下截图显示了shop场景中的AD按钮:

图 13.63 – 商店场景中的 AD 和开始按钮
提示:在商店场景的独立版本中我们需要 AD 按钮吗?Unity 支持它吗?
错误报告 – 重置玩家的生命值
我们收到了第二份报告,建议当游戏完成时,玩家的生命值不会重置。
为什么会发生这种情况,我们如何解决这个问题?
以下截图显示了第 1 级玩家的生命值计数器:

图 13.64 – 玩家的生命值
提示:这是否发生在你通过暂停屏幕退出游戏时?当所有生命值都丢失时,玩家的生命值会重置吗?
错误报告 – 第 3 级较慢的系统
当在较慢的系统上玩 Android 版本时,据报道,第 3 关的运行速度比第 1 关和第 2 关慢。
如果有任何,可以做出哪些修改来解决这个问题?
下面的截图显示了游戏减速的位置:

图 13.65 – 第三级的一半
提示:可以做出哪些修改而不会影响独立或性能更强的 Android 设备?
错误报告 – 有时,游戏结束得太快
一些错误测试人员报告说,在开始游戏时,游戏结束得比预期要早,玩家的飞船动画从屏幕中消失。
为什么会发生这种情况,如何进行修改?
下面的截图显示了玩家飞船的推进器在离开关卡过早时的末端:

图 13.66 – 玩家离开得太早
提示:这个问题发生在哪个关卡?是在 Unity 编辑器中发生的吗?是每次都发生吗?如果不是,你在做什么?有什么不同之处?
你可能可以通过 Google 搜索关键问题来解决这些问题。其他问题可能更具体,你可能需要在你代码中包含变量名的部分添加 Debug.Log(),以便在游戏的一定点后看到发生了什么变化。例如,GameManager.playerLives 在游戏的一定点是否调试了与预期不同的值?如果你使用 Microsoft Visual Studio 作为你的 IDE,你可能想开始添加断点并逐步执行你的代码以查看发生了什么变化。如果你不知道断点是什么,我建议你查看以下链接:docs.microsoft.com/en-us/visualstudio/debugger/using-breakpoints?view=vs-2019。
为了可能帮助解决这些性能问题,我们将检查分析器工具,看看它如何帮助我们检查游戏性能。
理解分析器工具
在本节中,我们将检查 Unity 编辑器的一个工具 – 分析器。这个实用的工具将向我们展示我们的游戏可能在系统资源需求方面出现峰值,以及我们的游戏何时一次性使用过多资源。
在深入了解之前,让我们打开分析器窗口,看看它的默认布局:
- 在 Unity 编辑器的顶部,选择窗口 | 分析器。
分析器窗口在 Unity 中表现得像任何其他新窗口。通常,分析器应在第二屏幕的全屏模式下运行良好。否则,将分析器与控制台一起停靠,如图下截图所示:

图 13.67 – ‘分析器’窗口
- 在 Unity 编辑器的顶部,按播放按钮。大约 5 秒(左右)后,按暂停按钮(运行的场景无关紧要)。
分析器窗口将变得活跃,显示一个图表和信息表。这将被分为四个部分,如下面的截图所示:

图 13.68 – ‘分析器’窗口的各个部分
让我们更深入地看看这些部分:
- 分析器模块 – 对每个可以添加的属性的测量值的模块分解。

图 13.69 – 分析器模块
-
分析器控制 – 通过控制导航来移动帧。
-
帧图表 – 在这个部分,我们可以点击并拖动鼠标来查看区域上的指示器。模块详细信息面板将列出在播放模式期间使用的资源更新(取决于选择的哪个模块)。
-
模块详细信息面板 – 选择每个分析器模块将在模块详细信息面板中提供更多详细信息。例如,如果选择CPU 使用率,将有一个“概述”,其中包含对哪些资源使用最多的详细分解。如果选择GPU 使用率,我们将获得两种不同类型的层次结构面板(层次结构和层次结构原始)。我们将在下一节中解释如何使用分析器和模块详细信息面板的情景。
让我们进一步了解分析器以及如何诊断性能峰值。
以下截图显示了一个高亮的峰值(标记为 i),以及指示器。我们可以取消选中“GPU 使用率”模块(标记为 ii)内的每个属性,以查看导致帧图表中峰值的原因。同时,更新模块详细信息面板(标记为 iii)列表以显示导致性能峰值的原因。

图 13.70 – 游戏运行时‘分析器’显示性能峰值迹象
在 UpdateDepthTexture.
通过快速 Google 搜索,我们可以看到为什么会出现这种情况,以及我们是否可以采取任何措施来修复问题。根据 Unity 论坛上的一个主题,此问题是由分析器本身引起的,这是可以接受的,因为我们不会在最终构建中需要分析器。尝试重现峰值,交叉比较,并检查多个答案以尽可能多地验证您的峰值是否相关。您很可能会找到一种方法来最小化/删除问题。
另一种检查此问题的方法,可能也是解决此问题的方法,是在 Unity 编辑器外运行我们的游戏,以删除任何正在使用的资源。处理此问题的一种方法是将我们的游戏作为独立(标记为 i)开发版本构建并运行(标记为 iii),并将其自动连接到分析器窗口(标记为 ii),如下面的截图所示:

图 13.71 – 'Killer Wave'在 Unity 编辑器外运行,带有'性能分析器'
如您所见,我们不再在我们的开发构建(如前一张截图中的*****所示)中出现此资源问题。
更多信息
如果您想了解更多关于性能分析器窗口的信息,请查看以下链接:docs.unity3d.com/Manual/Profiler.html。
正如我们所见,性能分析器窗口是一个有用的工具,它帮助我们纠正内存泄漏、垃圾回收以及任何其他可能的错误。
现在,我们将查看我们的最后一个 Unity 工具,我们可以用它来查看图形管线是如何用来显示我们的游戏的。
帧调试器
帧调试可以用来在 Unity 编辑器中显示我们游戏的每个帧是如何创建的。这可以帮助我们解决任何关于艺术作品显示的潜在着色器问题。然而,这也是一个健康的提醒,说明了场景是如何组合起来的,并挑战了可能使用的任何不必要的效果/材质。
要访问帧调试工具,请执行以下操作:
- 在 Unity 编辑器顶部,选择窗口 | 帧调试。
我们的帧调试窗口将出现。
-
现在,让我们从
Assets/Scene加载我们的title场景。 -
点击帧调试窗口顶部的启用按钮,以查看帧是如何创建的。
-
帧调试窗口将变得活跃,并显示正在实施的工具和属性列表。
-
在游戏窗口可见的情况下,将帧调试窗口中的滑块(以下截图中所突出显示)从右向左缓慢滚动,以查看此帧是如何创建的。
以下截图显示了带有高亮显示的启用按钮的帧调试窗口,以及三个步骤(4、8和26):

图 13.72 – '帧调试器'工具遍历游戏中的每个步骤
注意,步骤 4显示了应用于Bloom纹理的图像,以在步骤 26中创建闪亮的发光效果。
在完成每个步骤并查看所有地图、渲染目标和创建帧所需的其它所有必要步骤后,您还可以从帧调试窗口中选择绘制调用(对图形卡的调用),这将突出显示它所引用的游戏对象。
在以下截图中,我们有shop场景,总共有47个步骤,如帧调试窗口顶部所示。如果在帧调试窗口中选中了绘制调用(中间突出显示的矩形),它将在层次结构窗口中 ping 它所引用的游戏对象,如左侧所示:

图 13.73 – 帧调试器显示它所引用的游戏对象
更多信息
如果你想要了解更多关于帧调试器及其功能的信息,请查看以下链接:docs.unity3d.com/Manual/FrameDebugger.html。
希望你能充分利用帧调试器并调试任何图形问题,以及通过 Unity 更深入地理解图形管道。
在我们总结这一章之前,我们将逐一查看我们游戏测试人员提供的四个错误报告。
解决 bug – 答案
作为程序员,我们需要例如,跟踪一系列步骤中的值,以查看它是否是代码没有按预期执行的原因。然而,也有不同的测试执行方法,在项目代码更新后检查你或他人的代码也是一个好主意。
作为程序员,你可能会听到关于执行的不同类型的方法以及一个项目代码应该测试多少的内容。
这里是你在自己的项目和其它项目上将要执行的一些更流行的测试类型:
-
for循环或一种确保一小块代码正确工作的方法。 -
集成测试: 当多个代码部分(可能来自其他程序员)被合并在一起并测试以查看游戏运行时是否出现问题时,会使用这种类型的测试。
-
冒烟测试: 这些测试是为了确定当前构建是否稳定。这种类型的测试有助于测试人员决定是否继续进行进一步的测试。冒烟测试应该是最小化和频繁的。
-
回归测试: 当向项目中添加代码时,现有代码可能与新添加的代码发生冲突。在这里,你检查现有代码以确保更改或添加没有产生错误。这些测试可以手动运行小型项目,或者在大型项目中每次实施更新时运行一系列测试。
-
系统测试: 通常,在集成测试之后,会进行系统测试来检查整个项目代码是否存在缺陷以及代码的一般行为。
测试通常有助于你跟踪整个项目,而不仅仅关注其一部分。这也是为什么制定某种计划很重要的原因;例如,我们有我们的游戏设计简报。我们还可以更加技术性地拥有一个 UML 图来帮助我们查看脚本之间的连接。因此,我们不应该对编码有其他的想法。现在我们有了代码,我们希望改进它,使其更高效,并提醒自己 SOLID 原则。
说到 bug 测试,你有没有想过在解决 bug部分提出的我们游戏的四个错误报告的解决方案?希望你已经有了,因为我们现在将逐一查看它们。
错误报告 – "独立 AD 按钮"解决方案
如您所忆,我们有一个 shop 场景,其中有一个 AD 按钮。当按下时,玩家将观看广告并获得商店积分作为奖励。这在游戏的移动版本中运行良好,但已经有人报告说这个按钮在独立版本中不起作用。
这个问题的简短答案是,Unity 不支持独立构建的广告。这让我们要么寻找在游戏中添加广告的解决方案,要么关闭 AD 按钮游戏对象,无论是通过脚本还是手动通过 AD 按钮都会自动使 Start 按钮调整大小,多亏了 垂直布局组。为了解决这个问题,我们需要实施一些重新设计,而不仅仅是程序员的问题:

图 13.74 – 没有 AD 按钮的商店按钮布局
故障报告——“重置玩家的生命”的解决方案
为了正确重置玩家的生命,我们需要在 TitleComponent 脚本中应用一个修复,以便当我们的游戏从退出或玩家失去所有生命重新开始时,GameManager.playerLives 被重置回 3。
在 TitleComponent 脚本中添加以下代码以将玩家的生命重置回 3:
void Start()
{
if (GameManager.playerLives <= 2)
GameManager.playerLives = 3;
}
保存 TitleComponent 脚本。
故障报告——“第 3 级别较慢的系统”的解决方案
拥有多个设备来运行一系列测试的好处至关重要。如果您的游戏支持低规格设备,那么您也在吸引更广泛的受众。关于我们游戏的报告表明,设备在低功耗设备上表现不佳。为了解决这个问题,您需要确保以下内容:
-
后处理已禁用。
-
屏幕上同时出现的敌人更少。您可以通过在 检查器 窗口中更改
EnemySpawner的速度来实现这一点。 -
从场景中移除任何全局照明并应用基本照明。
-
从
shop场景中移除额外的背景。 -
修改
CameraMovement脚本的Start函数。从6秒等待时间改为7秒,以给设备更多的时间来加载。
故障报告——“有时游戏结束得太快”的解决方案
已有报告称,关卡结束得比预期要早,因此,而不是关卡持续 25 秒,据报道只持续了 5-10 秒。
这是因为 ScenesManager 脚本中的 BeginGame 方法没有将 gameTimer 变量重置为零。按照以下步骤修复此错误:
-
打开
ScenesManager脚本。 -
滚动到
BeginGame方法,并在方法顶部添加以下行:gameTimer = 0; -
保存
ScenesManager脚本。
测试您的代码,不断回顾它,并不断打磨它。继续寻找改进脚本的其他方法。接受最初几行代码可能不是您最好的,并且重新访问并持续优化代码是可以的。
进一步信息
如果你想要继续了解如何改进你创建的游戏的代码,请查看 Unity 提供的以下链接:learn.unity.com/tutorial/fixing-performance-problems#5c7f8528edbc2a002053b595。
这就带我们来到了本节的结尾,我们构建了游戏的独立版本,并检查了需要克服的潜在问题,这些问题是我们与虫害测试员一起解决的。之后,我们查看了分析器窗口,我们可以用它来监控游戏性能,以及帧调试器,它显示了制作一帧所遵循的步骤。然后,我们讨论了何时以及如何测试我们的游戏,在查看我们收到的错误以及如何纠正它们之前。
现在,让我们整体讨论这一章。
摘要
这一章是关于将我们在整本书中开发的游戏整合在一起,直到它达到尾声。我们讨论了如何通过添加除子弹或按钮之外的物理碰撞来推动我们的游戏进一步发展。我们设置了碰撞,这使我们更多地参与了调整 Rigidbody 组件,使我们的游戏对象以不同的方式表现。我们通过添加阻力和影响场景的重力来实现这一点。
然后,我们继续讨论如何通过更新 Canvas Scaler 来改进我们游戏的屏幕比例,以及它如何使我们的 UI 在不同比例下看起来更稳定。我们还使用不同的 Unity 函数,如WorldToViewportPoint,使我们的游戏区域在不同分辨率下更加灵活。
在这个阶段,我们的移动版本已经准备好构建和测试,以便我们可以看到它使用更新的触摸屏控制运行得如何。我们还研究了其在纹理方面的优化,并将它们压缩以减小游戏的大小,使其整体运行得更好。
在移动版本构建之后,我们检查了 PC 版本,并对游戏的外观进行了更多改进。我们这样做是因为独立机器可能拥有更强大的 CPU、显卡、内存等。然后,我们添加了如后处理等效果,以改变游戏的外观和感觉,使其更加精致。我们通过从照明设置窗口添加全局照明和雾化效果来继续为我们的游戏添加更多精致。这使得我们的材料闪耀红色,并透过雾蒙蒙的黑暗,赋予它们更多未来感。我们还向最终关卡添加了反射雕像。这些使用了反射探针组件。之后,我们讨论了如何从其反射纹理的大小方面优化它。
最后,我们研究了构建和测试我们的独立版本,并介绍了一些虫害测试场景,其中我们的虫害测试员发现了某些事物运行不正常的问题。我们一起审查并解决了这些问题。
制作一个游戏并不容易,制作游戏的方法有很多。总有人会比你的方法更好,并且可能会找到它的漏洞。然而,正如本章所述,游戏可以在每个阶段制作并改进;最糟糕的事情就是试图第一次就制作出一个完美的游戏。如果你这样想,你最终可能一个游戏都做不出来。
你准备好进行完整的模拟测试了吗?你应该准备好了;通过阅读(并希望重现)这本书来检验你记住了什么。如果你对某些无法回答的问题有任何问题,每个问题旁边都有参考编号(例如:CH1,表示第一章)来帮助你回忆。这一切都会在下一章中详细解释——享受它吧。
第十四章:第十四章: 完整 Unity 程序员模拟考试
欢迎参加完整的 Unity 程序员模拟考试。在这里,我们将提供一系列多项选择题,类似于你在本书每几章的结尾所回答的问题。
尽你所能,看看结果如何——目标是答对所有问题。如果你完成了这个模拟考试,你将准备好参加真正的 Unity 程序员考试。如果你因为任何原因答错了一个问题,回到这本书或 Google 上尝试找到答案。尽量避免直接跳到附录中的答案;更多地使用你的知识而不是肌肉记忆。理解问题很重要,这样你才能回答它。这些模拟问题只是你在考试中可能会遇到的问题类型。
我建议至少阅读每一题两次;有时候,考试题目会试图通过问题的措辞来迷惑你。考试还可能通过让你阅读一大段代码来试图消耗你的时间,而这实际上并不是必需的。
考试是计时进行的,但你应该有足够的时间回答每个多项选择题。最好在开始和完成这个模拟考试时留出一些个人时间。如果你遇到困难,可以先跳过问题,稍后再回来。有时候,通过一次解决所有问题,更容易把简单的问题排除掉。如果你仍然有困难,我在每个问题的末尾用括号标出了你可以从中获取更多信息的内容/附录。
因此,慢慢来,不要被措辞迷惑,继续前进...如果你喜欢它,那也是一个额外的奖励!
完整模拟考试
-
你们中的一名初级程序员请求一个全局实例,该实例可以在游戏的任何代码中访问。当你有代码可以作为管理器时,哪种设计模式可以满足这一要求?(CH1)
-
原型
-
抽象工厂
-
单例
-
构建者
-
-
你的开发主管要求你实现每当玩家穿过门口时,门旁边的一盏灯会亮起。你已经编写了一个
OnTriggerEnter()脚本来启用灯光,并在你的门口上添加了一个带有Rigidbody的碰撞器。你的玩家也有碰撞器和Rigidbody。当你运行游戏以测试你的代码时,灯光没有亮起。
这个问题的最可能原因是什么?(CH2)
-
门口的碰撞器没有被标记为触发器。
-
光游戏对象也需要一个触发器。
-
你不需要多个
Rigidbody组件。 -
门口没有连接到灯光。
-
哪个函数允许我们从任何方向读取
Vector3或Vector2,但保持其大小仅为一个?(CH7)-
Normalize() -
MoveTowards() -
Lerp() -
Scale()
-
-
我们已经得到了我们的游戏设计文档(GDD),在其中,它指出我们的玩家角色(PC)将需要奔跑和躲避向他们投来的多个海滩球。海滩球可以相互弹跳。场景中还有各种静态道具和一个 PC 抓取健康的触发区域。
在这个场景中,哪些对象至少需要一个Rigidbody组件?(CH2)
-
PC 和海滩球
-
PC 和不可见健康区域
-
仅海滩球
-
海滩球和不可见健康区域
-
关于海滩球的问题,在我们的项目层碰撞矩阵(编辑 | 项目设置 | 物理)中,当它们相互作用时,我们应该检查矩阵中我们的哪个碰撞器的物理?(CH13)
-
PC 和海滩球,海滩球和海滩球,以及 PC 和健康区域
-
PC 和海滩球
-
海滩球和健康区域,海滩球和海滩球
-
PC 和健康区域
-
-
我们有一个需要支持低性能平板电脑的移动应用程序。在我们的应用程序中,我们将有一个迷你游戏,玩家将能够将无限数量的篮球投向篮筐。然而,只有 10 个篮球在摄像机的视野中。因此,程序员已经这样设计,即使用相同的 10 个篮球而不是在每次投掷时实例化一个新的篮球。
程序员知道用于重用篮球资源的设计模式有一个名称。这个设计模式的名称是什么?(CH1)
-
抽象工厂
-
对象池
-
依赖注入
-
构建者
-
你被分配制作一个在应用程序启动时播放音乐的程序。当用户浏览不同的场景时,音乐不会被移除或受到影响,并继续播放。你已经通过创建一个游戏对象并将其添加到播放音乐的脚本中开始了这个程序。
我们还需要做什么来确保我们的音乐继续播放?(CH3)
-
在起始场景中创建两个包含音乐脚本的游戏对象。
-
每个场景都需要一个带有脚本的音乐游戏对象。
-
在音乐脚本中的
Awake函数中添加DontDestroyOnLoad(this.gameObject);。 -
在每个场景中实例化
music游戏对象。 -
在 Unity 的 Cloud Build 中,我们应该检查什么来找到最新的构建?(CH1)
-
cloudBuildTargetName -
buildNumber -
bundleID -
scmCommitID
-
-
我们有一个手势控制的移动游戏,我们希望实现从屏幕的一侧滑动到另一侧来释放事件。
从Touch结构测量玩家手指按下位置的最佳方法是什么?(CH13)
-
Type -
Phase -
deltaPosition -
fingerId -
我们的首席程序员正在尝试找到一种简单的方法,让我们的飞行模拟器游戏涵盖移动、PC 和游戏控制台的控制。
实现这一点的最简单方法是什么?(CH13)
-
为每个游戏创建一个支持的项目。
-
为每个平台创建一个类。
-
从标准资产导入
CrossPlatformInput包。 -
使用
#define指令为每个可能的平台创建一个自定义输入管理器。 -
在我们的飞行模拟器中,我们刚刚将水平和垂直控制与
Input.GetAxis("Horizontal")和Input.GetAxis("Vertical")连接起来。游戏设计师指出,控制响应缓慢,我们需要使动作更灵敏。(第二章)
我们需要在输入管理器中更改哪些设置来改进/修正这一点?
-
反转
-
灵敏度
-
轴
-
重力
-
我们现在正在制作一个横向卷轴平台游戏,我们希望按下A按钮时可以跳跃,但我们还希望按下上箭头键时也可以跳跃。在输入管理器中,我们在哪里添加第二个跳跃按钮?(第二章)
-
正按钮
-
负按钮
-
负按钮
-
正按钮
-
-
在我们的平台游戏中,我们注意到玩家可以左右跑动,但当我放下游戏手柄时,我的玩家角色会慢慢向右移动。我相信我需要在输入管理器中更改一个设置,但应该是哪一个?(第二章)
-
敏感性
-
快照
-
重力
-
死亡
-
-
通常,在键入实例变量名称时,我们使用什么命名约定?(第二章)
-
位置表示法
-
全大写
-
蛇形命名法
-
驼峰式命名法
-
-
已经为你发布了一个错误修复,游戏设计师在 Unity 编辑器中更改了一个公共
int变量,并将其设置得太高。这个变量没有理由超过 100。
程序员应该使用哪个属性来限制游戏设计师?(第二章)
-
[GUITarget] -
[TextArea] -
[范围] -
[Header] -
你已经被技术负责人分配了一个任务,要存储 800 个非玩家角色(NPC)预制体。这些预制体中的任何一个都可以被选中并拖放到游戏中四处游荡。这个系统需要对我们设计师友好,理想情况下,所有 NPC 的选择都应该来自检查器窗口。还有可能敌人的数量将从最初的 800 个增加。
你将如何准备发布这些 NPC?(第二章)
-
简单地创建一个脚本,存储一个公共数组,以便容纳每个预制体的创建。
-
创建一个包含引用预制体的数组的可脚本对象。
-
创建一个包含私有序列化字段列表的类,该列表包含 NPC 预制体,并且每个 NPC 类在运行时创建一个 NPC 实例。
-
在运行时创建所有 800+个 NPC 并将它们存储在摄像机视图之外。
-
如果我们想在更新中某个特定时间计算 Unity 的物理,我们应该在时间管理器中更改哪个设置?(第十章)
-
允许的最大时间步长
-
固定时间步长
-
允许的最大粒子时间步长
-
时间缩放
-
-
哪个碰撞器最有效?(第二章)
-
圆柱体
-
球体
-
网格
-
盒子
-
-
我们正在模拟一块巨石从天空中坠落,以及一包薯片。我们希望巨石坠落得更快。
我们在哪个游戏对象的Rigidbody组件中更改了哪些设置?(第十三章)
-
减少薯片的重量并增加巨石的阻力。
-
增加薯片包的角动摩擦系数,并增加巨石的重量。
-
增加巨石的重量,并减少薯片包的重量。
-
减少巨石的阻力,并增加薯片包的阻力。
-
我们会在什么情况下使用触发器而不是仅仅使用碰撞器?(第二章)
-
当角色坐在健康区域充电能量时。
-
如果两个游戏对象发生碰撞,但我们只想手动设置我们的粒子效果。
-
只有在我们需要在运行时更改我们的
Rigidbody设置时。 -
当多个碰撞器是另一个碰撞器的子对象时。
-
-
你正在制作一款科幻街机射击游戏。玩家的视角有一个围绕屏幕的 UI 显示,显示有关我们的任务和飞船健康状况的大量重要细节。在屏幕的左下角,我们有一个关于我们飞船状况的 3D 视图。每当飞船受到伤害时,我们可以在 3D 视图中看到结果,以及粒子效果来强调伤害。在 3D 视图中,飞船上每个可能的损伤点都有一个碰撞器,如果导弹击中它,该碰撞器会做出反应。
在测试中,你发现 3D 视图中飞船的敌导弹在应该损坏你的飞船时却从你的碰撞器上弹回。
程序员应该如何解决这个问题,同时保留游戏内对象和 UI 的功能?(第十三章)
-
在层碰撞矩阵中,关闭 3D UI 飞船层与导弹和小行星层的碰撞。
-
增加导弹的重量,使其能穿过碰撞器。
-
在飞船上的所有碰撞器上添加第二个碰撞器,以增加其概率。
-
在飞船周围添加一个主要碰撞器,以便当导弹击中它时,这将暂时禁用内部的所有碰撞器。
-
你已经搬了工作室,并加入了一个新的游戏项目,在那里你身处沙漠,保卫堡垒,抵御 6000 头驴子冲来摧毁你的文明。你唯一的防御手段是扔出沉重的湿豆袋来耗尽驴子的体力。为了瞄准每一头驴子,你使用一个射线路径系统,该系统与任何一头驴子的网格碰撞器接触。现在,你的开发主管要求增加至 20000 头驴子以提高难度。你现在开始注意到游戏的性能急剧下降。团队中的每个人都正在努力,以确保所有 20000 头驴子都能进入游戏。
你能做出哪些改变来提高游戏性能?(第二章)
-
让驴子稍微大一点,以便在环境中占据更多空间。
-
将网格碰撞器替换为球体碰撞器。
-
创建一个特殊的驴子层掩码,以便忽略环境中的其余部分。
-
缩短你的射线路径长度。
-
永远不要让驴子跑得飞快。
-
你的团队成员已经为你游戏的某个新部分提交了一个 commit。在这个阶段,建议确保你的整个游戏运行正常。
你的开发主管要求你测试你的游戏。你应该进行哪种测试?(第十三章)
-
烟雾
-
集成
-
回归
-
系统
-
你被要求通过创建自定义方法来检查别人的代码的特定部分,以确保返回值是正确的。
有一个特定的测试类型名称,是什么?(CH13)
-
静态测试
-
可访问性测试
-
单元测试
-
后端测试
-
单元测试有哪些好处?(CH13)
-
它将检查你的全部代码。
-
它测试单个“单元”的代码。
-
如果定期执行,它只需要测试最新的代码。
-
单元测试揭示了函数之间代码的效率。
-
-
当使用
MinMaxCurve时,从性能角度来看哪个属性最节省成本?(CH4)-
优化曲线
-
在两个常量之间随机
-
在两个曲线之间随机
-
常量
-
-
以下哪个选项会阻止粒子系统支持过程模式?(CH4)
-
禁用循环。
-
将
Simulation Space属性设置为World。 -
取消勾选
Auto Random Seed复选框。 -
启用碰撞。
-
-
一位艺术家找到你,要求你对正在制作的科幻游戏进行视觉上的修改。他要求你随着一系列小行星向行星靠近而逐渐缩小它们。
小行星来自粒子系统发射器;哪个模块适合艺术家的需求?(CH4)
-
渲染器
-
纹理表动画
-
子发射器
-
生命周期大小
-
整个开发团队几乎完成了他们的“斯皮特菲尔英国战役”游戏的制作,并希望在每架斯皮特菲尔飞机的后面添加粒子效果作为最后的修饰。
其中一位游戏设计师建议斯皮特菲尔的烟雾应该随机改变以强调风的不稳定性。你已经编程到粒子效果能够检测到风,但你应该对粒子烟雾进行什么操作以显示风正在影响烟雾?(CH13)
-
在Rotation by Speed模块中修改Angular Velocity属性。
-
将
0设置为。 -
在Noise模块中增加Strength属性。
-
修改Size属性在Size by Speed模块中的曲线。
-
在你开发的独立游戏中,你将场景设置得使得环境光照强度乘数设置为
0.75。当玩家完成关卡并进入下一个场景时,光照被设置为1.24。你正在使用LoadSceneAsync,其LoadSceneMode为累加模式。
当你加载下一个场景时,光强度将被设置为多少?(CH3)
-
1.24 -
0.75 -
1 -
0 -
你又一次搬了工作室,开始制作一款开放世界游戏,玩家可以在数英里内行走。由于场景可能过于庞大而带来的复杂性,你决定将场景分割成多个部分。当进行场景切换时,玩家将被加载到下一个场景中。
哪个函数允许我们使游戏对象移动到另一个场景?(CH3)
-
CreateScene() -
MoveGameObjectToScene() -
MergeScenes() -
SetActiveScene() -
当涉及到存储数据时,对于
PlayerPrefs来说,哪种选择更可能?(第十一章)-
购买信息
-
监视器分辨率设置
-
用户电子邮件地址
-
登录密码
-
-
你可以在不模拟(本地)的情况下保存哪种类型的变量到
PlayerPrefs?(第十一章)-
浮点数
-
双精度
-
枚举
-
数组
-
-
当序列化数据到设备的本地磁盘空间时,你会使用哪个系统命名空间?(第十一章)
-
Linq
-
IO
-
数据
-
集合
-
-
在我们科幻游戏的最后,我们将所有统计数据以 JSON 格式从
PlayerStats类保存到本地磁盘空间。但当我们想要从存储中检索 JSON 文件时,我们应该用什么来替换缺失的间隔?(第十一章)
JsonUtility.FromJson<____>(stringFromFile);
-
StatsInfo -
String -
Array -
PlayerStats -
当从互联网检索图像时,我们使用哪个 UI 组件来显示结果?(第九章)
-
画布
-
原始图像
-
图片
-
面板
-
-
哪个 UI 组件将一系列 UI 元素按固定距离排列成一行?(第九章)
-
垂直布局组
-
水平布局组
-
网格布局组
-
画布组
-
-
我们已经使我们的游戏能够从 Unity 仪表板的远程设置部分更新几个统计数据。
我们可以使用以下哪些值?(第十一章)
-
Char -
string -
List -
UInt16 -
以下哪个平台可以使用 Unity Analytics?(第十一章)
-
PS4
-
科摩多阿米加 500
-
安卓
-
Xbox
-
-
当你将你的游戏连接到 Unity Analytics 时,哪个事件会自动开始提供每日报告?(第十一章)
-
核心事件
-
标准事件
-
自定义事件
-
交易事件
-
-
在我们的第一人称射击(FPS)游戏中,我们刚刚连接了我们的海军陆战队员的空间大炮的有限状态机,以便发射弹丸。
当大炮开火时,会有砰的一声巨响,粒子效果从大炮中喷涌而出,并且发射出光束。
目前,光束唯一能接触到的只有墙壁,它上面附着了一个碰撞器。
当光束击中墙壁时,当表面撞击时还会触发另一个粒子。在这场爆炸中,我们缩小然后摧毁光束。
我们应该在哪个事件中期待光束被摧毁?(第四章)
-
OnStateExit -
OnStateEnter -
OnStateMove -
OnStateUpdate -
你正在为 SWAT 团队游戏原型化一个第三人称角色,我们需要让一些基本控制开始运行。我们的角色目前设置为在所有方向上奔跑、倾斜和射击。理想情况下,我们希望我们的角色可以射击和跳跃,或者射击和倾斜。目前,我们的基础移动设置为覆盖混合,而其他层设置为添加混合。
应该按什么顺序设置动画层?(第四章)
-
射击、倾斜、基础移动
-
基础移动、射击、倾斜
-
倾斜、基础移动、射击
-
射击、基础移动、倾斜
-
我们有一个玩家从站立到蹲下的动画。我们希望每个动画之间的动画正好持续 0.8 秒。
在动画过渡中,我们需要关注哪些属性?(第四章)
-
过渡持续时间和过渡偏移量
-
固定持续时间和过渡持续时间
-
有退出时间和固定持续时间
-
有退出时间和退出时间
-
当涉及到在 Unity 中动画化面部时,哪个混合树是最好的?(CH4)
-
2D 自由形式笛卡尔
-
直接
-
2D 简单方向
-
1D
-
-
在你最新的独立游戏开发中,你一直专注于动画控制器的过渡。你的过渡顺序如下:
-
闲置到哭泣
-
闲置到跳过
-
闲置到打喷嚏
-
闲置到笑
-
你的动画过渡属性设置为以下内容:
-
中断来源:当前状态
-
有序中断:勾选
你当前的过渡设置为跳过。在运行时,你的角色已经开始跳过,但作为测试者,你也按下了所有四个按钮来触发每个动画状态。
哪个过渡将具有优先级?(CH4)
-
闲置到笑
-
闲置到打喷嚏
-
闲置到哭泣
-
闲置到跳过
-
你正在创建一款第一人称射击游戏(FPS),你现在正在处理玩家的相机,并确保当玩家靠近时,他们的武器不会穿过物体。
所有玩家的武器都设置在一个名为FPS的层上。然后你设置相机的剔除遮罩以查看除了FPS层之外的所有内容。
接下来,你创建第二个相机,并将其剔除遮罩设置为仅渲染FPS层,其清除标志设置为?(CH9)
-
深度仅
-
实色
-
不清除
-
天空盒
-
作为一名热情的独立开发者,你决定制作游戏“沙漠巴士”的精神续作。你几乎完成了游戏开发,并决定添加一些优化。你决定让任何较小的 3D 资产,如小石子、小植物和昆虫,在远距离时不渲染,只有在近距离时才渲染。无论我们离它们有多远,我们都应该仍然能看到较大的资产。
你会用哪个相机属性来帮助实现这一点?(CH2)
-
farClipPlane -
layerCullDistances -
cullingMatrix -
useOcclusionCulling -
当编写自定义卡通阴影边缘检测效果脚本时,相机的
DepthTextureMode应该设置为多少?(附录)-
无
-
DepthNormals -
MotionVectors -
Depth
-
-
我们目前正在开发一款游戏,我们的玩家可以通过 Unity 的主相机看到,并且具有临时放大的能力。
我们相机的哪个属性允许我们放大?(CH2)
-
targetDisplay -
aspect -
lensShift -
fieldOfView -
你在你的游戏中引入了多人分屏模式。你现在正在将屏幕分为两行。
程序员应该如何设置相机的视口矩形选项?(CH2)
-
设置两个相机的
1和0.5。设置玩家 1 的0.5和玩家 2 的Y为 0。 -
设置两个相机的
1。设置玩家 1 的0.5和玩家 2 的1。设置两个相机的0.5。 -
设置两个相机的
0.5和1。设置玩家 1 的1和玩家 2 的0.5。 -
设置两个相机的
1和0.5。设置玩家 1 的1和玩家 2 的0.5。 -
我们的游戏出现了一些冲突,因此作为预防措施,我们需要加强安全性。以下是一个代码示例:
public class SuperFox : MonoBehaviour { public float superAttack = 1f; public float superFly = 5f; }
最好的方法来保持这两个变量安全是什么?没有其他类使用它们,但我们需要从检查器中访问它们以供游戏设计师使用。
-
将访问修饰符从
public更改为private。 -
将访问修饰符从
public更改为private并添加[SerializeField]属性。 -
将访问修饰符更改为
private并为其提供get和set访问器 -
public本身就足够安全;它只是几个浮点数 -
我们有一个基于物理的对象,我们希望它围绕一个特定的点旋转,比如门。
哪种类型的关节将允许这种类型的运动?(CH2)
-
角色
-
固定
-
轴承
-
弹簧
-
我们从一个艺术家那里得到了一个灯具资产,他们要求我们在游戏中使灯具的光闪烁。
我们需要在脚本中操作我们灯光的哪个属性来实现闪烁效果?(CH2)
-
Mode -
spotAngle -
range -
intensity -
你正在测试一个场景并向其应用不同的灯光。在场景中,你有一系列游戏对象:
-
装饰灯
-
太阳
-
一辆警示灯亮着的汽车
-
一个车库
-
车停在车库中。车库内部周围是装饰灯,阳光透过车库门照射进来。
你已经启用了全局光照以增加太阳的真实感。尽管这看起来很令人印象深刻,但你的场景现在占用了大量的内存使用。
我们如何保持场景看起来令人印象深刻,同时继续保持低内存使用?(CH13)
-
将阳光设置为
0。 -
将灯光的光照模式更改为烘焙。
-
将装饰灯设置为
0,即汽车的警示灯。 -
在光照设置中禁用实时全局光照。
-
一个艺术家要求我们使用脚本将霓虹灯的发光从红色改为蓝色。
我们可以使用哪个属性来改变我们标志的发光?(CH4)
-
_EmissionColor -
_Color -
_SrcBlend -
_EmissionMap -
你被要求向一个闪亮的走廊添加一个反射的玻璃球效果,该走廊有大型、开阔的白色银色窗户。建筑外面是一个明亮、阳光明媚的背景,草地、灌木丛和树木都应用到了天空盒中。走廊包含一系列反射探针。
应该在走廊的网格渲染器组件上使用哪个反射探针选项来创建一个闪亮的反射表面?(CH13)
-
简单
-
混合探针和天空盒
-
混合探针
-
关闭
-
我们的走廊地板因为反射探针而看起来闪亮且具有反射性。我们还注意到墙壁似乎与反射地板不同步。
我们需要更改关于我们的反射探针的哪个设置来解决这个问题?(CH13)
-
启用盒子投影
-
提高分辨率
-
提高重要性
-
启用 HDR
-
您的艺术家为雪地级别创建了 3D 资产,并尝试使用之前游戏中的资产。艺术家制作了一个木屋,并将该资产放入游戏中。整体场景看起来很棒,但场景中的整体颜色并不协调。
我们后期处理堆栈中的哪个属性可以帮助使场景中的颜色统一,从而给我们的资产带来整体冰冷的视觉效果?(CH13)
-
用户 LUT
-
粒子
-
色差
-
暗角
-
我们有一个技术演示,玩家在科幻走廊中行走,阳光透过窗户照进来,照亮了走廊。我们已经从我们的后期处理堆栈中应用了光晕效果。我们希望走廊中的某些游戏资产能够像光晕效果预期的那样发光和闪烁。一位艺术家通知你,所有艺术资产都有发射贴图,但它们的级别各不相同。
我们需要改变哪个属性才能使我们的光晕效果覆盖低级别的发射贴图?(CH13)
-
减少阈值
-
增加强度
-
减少软膝盖
-
增加半径
-
在运行时,哪个函数可以将两种材质从一种过渡到另一种?(CH9)
-
SetColor -
Lerp -
SetFloat -
EnableKeyword
-
-
我们正在制作一款第一人称生存恐怖游戏。玩家在走动时有一个手电筒,用于在鬼屋周围行走。我们的设计师要求手电筒的末端在投射到的表面上产生玻璃图案。
哪个光属性将支持设计师的要求?(CH2)
-
cullingMask -
cookie -
spotAngle -
type -
您的 PC VR 游戏开发已达到终点。您目前将 Camera 组件的渲染路径设置为前向。
以下哪个后期处理属性有助于降低使用 VR 应用程序时的恶心风险?(附录)
-
景深
-
部分乘法
-
饱和度
-
抗锯齿
-
在开发 VR 游戏时,开发者应该针对多少光子延迟的运动水平来欺骗玩家的思维,使他们认为自己身处另一个地方?(附录)
-
80
-
20
-
35
-
50
-
-
我们正在开发一个移动 VR 游戏,其中包含单色材料和未照明着色器的艺术资产。此外,我们的相机组件的渲染路径设置为前向。
哪个抗锯齿设置可以改善游戏视觉效果,但可能不会影响性能,使其处于不可玩的状态?(附录)
-
4x 多采样
-
8x 多采样
-
12x 多采样
-
禁用
-
Unity 编辑器中的哪个窗口将提供我们项目的绘制调用列表,并允许我们逐帧遍历它?(CH13)
-
分析器
-
帧调试器
-
服务
-
统计
-
-
我们创建了一个 VR 游戏,我们的玩家在一个有锁的门房间里,门无法打开。房间里有一扇窗户,外面是一片草地,远处有山脉。
玩家场景中的所有内容都是 3D 建模和纹理化的,还有一个单向光源。
当玩家走向窗口时,由于绘制调用激增,游戏开始出现卡顿。
我们如何提高我们游戏的表现力?(附录)
-
移除远处的 3D 模型,并用渲染的天空盒替换它们。
-
在纹理导入设置中禁用生成 Mip Maps,对所有远处的纹理进行设置。
-
添加灯光。
-
为了帮助减轻 VR 中的恶心感,开发者需要为他们的游戏设定多少每秒帧数(FPS)?(附录)
-
90 FPS
-
30 FPS
-
60 FPS
-
75 FPS
-
-
在帧调试器窗口中,开发者选择游戏对象几何形状的绘制调用。
在哪个窗口中对象被突出显示?(第十三章)
-
曲线编辑器
-
项目
-
层级
-
控制台
-
设计师想要更改您游戏角色的脚本参数,并将它们本地保存到磁盘上。
什么是最安全且最简单的方法给设计师访问权限?(第十一章)
-
将数据作为公共变量存储在MonoBehaviour中。
-
将数据保存在 ScriptableObject 中。
-
将数据保存在 PlayerPrefs 中。
-
创建一个带有游戏可以读取的 API 的 Web 服务。
-
如果我们正在编写依赖于特定组件的脚本,我们应该添加哪个属性?(第二章)
-
[Include] -
[Range] -
[SerializeField] -
[RequireComponent]
-
-
哪种方法会返回一个
Touch结构体?(第十三章)-
GetKeyDown() -
touchSupported -
Input() -
GetTouch()
-
-
关于
Input类,GetMouseButtonUp和GetMouseButton之间有什么区别?(第十三章)-
如果鼠标按钮被按下,
GetMouseButton将返回true,而GetMouseButtonUp将在鼠标按钮释放的帧中仅返回true一次。 -
GetMouseButton将返回一个Int,指示哪个鼠标按钮被按下,而GetMouseButtonUp如果任何鼠标按钮被按下,将返回true。 -
如果鼠标已连接,
GetMouseButton将返回true,而GetMouseButtonUp将在鼠标按钮按下时返回true。 -
GetMouseButton将返回一个包含可用鼠标按钮的数组,而GetMouseButtonUp将返回一个索引,指示哪个鼠标按钮被按下。
-
-
我们如何通过粒子系统提高性能?(第四章)
-
减少粒子的数量和大小,以减少屏幕上需要 alpha 混合的像素数。
-
增加粒子寿命,以便在内存中重用更多粒子而不是生成新的粒子。
-
减慢粒子的速度,以减少所需的物理更新次数。
-
在两个曲线之间而不是两个常量之间随机化属性,以减少需要生成的随机数的数量。
-
-
以下哪种方法对于屏幕顶部的生命条来说是最理想的?(第八章)
-
Horizontal -
Radial90 -
Radial180 -
Span
-
-
你正在开发一个第三人称游戏,你的玩家将与其他角色互动和交谈。你目前正在开发当玩家在 3D 世界中交谈时出现在他们头顶上的语音框。
哪个画布属性最适合用于 3D 环境中的语音?(CH13)
-
屏幕空间相机
-
像素完美
-
世界空间
-
屏幕空间叠加
-
哪个适合正交相机的使用?(CH9)
-
第一人称玩家视角
-
一个显示游戏世界概览的地图界面
-
一个 3D 空间战斗
-
一个敌人 AI
-
-
我们正在开发一款所有玩家和 NPC 都基于户外的策略游戏。你需要创建一个昼夜循环,因此在你的场景中,你有一个天空盒和一个方向性光源。
你将如何实现这一点?(CH2)
-
旋转方向性光源。
-
调整方向性光源的阴影设置。
-
更新 TimeManager 设置。
-
切换天空盒。
-
以下哪个会改变单个物体的材质属性?(CH4)
-
Renderer.shader -
Renderer.instance -
Renderer.material -
Renderer.sharedMaterial
-
-
创建发光熔岩湖的最佳方法是什么?(CH4)
-
为熔岩创建一个自发光纹理,并在材质上动画化 UV。
-
创建一个屏幕空间着色器,将熔岩效果应用到所需区域。
-
在熔岩移动的区域添加数十个点光源。
-
创建一个粒子效果来模拟熔岩的运动。
-
-
我们的游戏有一个过场动画,我们希望它在特定的音量级别播放音乐。
实现这一点的最佳方法是什么?(CH10)
-
当游戏播放过场动画时,切换到不同的音频混音快照。
-
当过场动画开始时,更改播放音乐的音频源的音量,并附加一个脚本组件以消除高频。
-
当过场动画开始时,在游戏区域周围应用混响区域。
-
当过场动画开始时,将更改应用到玩家存储的音量控制
PlayerPrefs中。 -
一个用于低性能机器的 VR 游戏使用前向渲染路径。以下哪个选项可以在帧率略有下降的情况下提高游戏的整体视觉效果?(CH13)
-
抗锯齿
-
实时反射探针
-
延迟渲染
-
聚光灯
-
-
你开发了一个移动应用,它从一个用户可以配置其体验的 UI 开始。一旦配置完毕,应用就进入 VR 模式。
我们如何让这个应用一开始就以非 VR 模式启动?(附录)
-
在场景中放置一个相机并将其 FOV 设置为 null。
-
将构建平台改为 PC 而不是 VR 平台。
-
在游戏开始时将
Time.timeScale设置为 0,并在第一帧渲染后添加一个脚本以搜索 VR 设备。 -
将
None添加到 VR SDK 列表的顶部。 -
如果 VR 设备以 90 Hz 渲染,延迟应该低于多少毫秒?(附录)
-
11
-
12
-
13
-
14
-
-
你开发了一个 VR 游戏,你的测试人员报告说游戏在某些部分卡顿。你的相机的渲染路径设置为前向(Forward)。你决定在运行时不重新分配眼纹理的内存,以避免任何其他性能冲突。
如果检测到帧率下降,我们可以在我们的XRSetting类中设置一个属性来帮助解决这个问题。
哪个属性最有效?(附录)
-
occlusionMaskScale -
eyeTextureResolutionScale -
renderViewportScale -
useOcclusionMesh -
你正在做一个包含几个脚本的个人小项目。你打开其中一个脚本,将其公共列表(List)改为私有。你保存了脚本,现在在控制台窗口中遇到了
NullReferenceExeception异常。
根据你提供的信息,什么原因导致了这个错误发生?
-
私有(private)需要首字母大写。
-
原始的公共列表(List)不能再从其他脚本中访问。
-
很可能是一个错误,需要报告。
-
有拼写错误。
-
在下面的屏幕截图中,我们有一个包含单独图像组件的场景。两个彩色方块的比例完美:
![Figure 14.1 – 问题 92 的屏幕截图]

Figure 14.1 – 问题 92 的屏幕截图
哪个单一组件可以如此完美地填充这两个图像组件?
-
水平布局组(Horizontal Layout Group)。
-
单个组件无法实现。
-
将方块放大并手动对齐。
-
在白色背景上应用 3D 四边形,并用顶点捕捉围绕它们绘制。
-
我们今天正在做一些用户界面(UI)的工作。我们被要求制作一个进度条,它将从底部开始填充,一直移动到顶部。
哪些属性一开始看起来是正确的?
progressChargeBar = GetComponent<Image>();
progressChargeBar.fillMethod = Image.FillMethod.Vertical;
progressChargeBar.fillAmount = 0;
progressChargeBar.type = Image.Type.Filled;
progressChargeBar = GetComponent<Image>();
progressChargeBar.fillMethod = Image.FillMethod.Radial180;
progressChargeBar.fillAmount = 100;
progressChargeBar.type = Image.Type.Simple;
progressChargeBar = GetComponent<Image>();
progressChargeBar.fillMethod = Image.FillMethod.Horizontal;
progressChargeBar.fillAmount = 50;
progressChargeBar.type = Image.Type.Sliced;
progressChargeBar = GetComponent<Image>();
progressChargeBar.fillMethod = Image.FillMethod.Vertical;
progressChargeBar.fillAmount = 0;
progressChargeBar.type = Image.Type.Tiled;
- 同事请求帮助,他们的公交车原型运行缓慢。
这里是代码的一个示例:
public class BusRacer : MonoBehaviour
{
public int playerCount;
public Bus smallBus;
private void Start()
{
CreateAndPlaceBus();
}
private void Update()
{
Bus[] buses = FindObjectsOfType<Bus>();
foreach (Bus b in buses)
{
b.Drive();
}
}
private void CreateAndPlaceBus()
{
for (int i = 0; i < playerCount; i++)
{
var busDriver =
Object.Instantiate(smallBus);
busDriver.transform.position =
StartingPosition();
}
}
private Vector3 StartingPosition()
{
//TODO Needs filling with content
return Vector3.zero;
}
}
public class Bus : MonoBehaviour
{
public void Drive()
{
//TODO drive fast
}
}
你能看出前述代码中导致性能问题的原因吗?
-
将
FindObjectsOfType从Update中移除,并在Start()中缓存一次。 -
将游戏中的公交车数量减少作为你的主要任务。
-
代码看起来没问题,所以检查场景中的资源(纹理、多边形数量等)。
-
Drive()为空,导致内存泄漏。 -
以下脚本已被分配给你,需要重构为“MVC”模式。如果你不了解模型-视图-控制器(Model View Controller)模式,它包括将你的项目分割成:
-
模型(Data,换句话说,变量)
-
视图(Interface/Detection,换句话说,UI)
-
控制器(Decision/Action,换句话说,
transform.rotation/transform.position)
-
这里是代码的一个示例:
public class DrunkenMaster : MonoBehaviour
{
private string talk;
private float drinkMeter;
private Vector2 location;
private float runSpeed;
private float punchStrength;
private float kickStrength;
private float rolyPolySpeed;
public MeshRenderer mesh;
public TextMesh label;
public void Update()
{
//Get Input
float kickForce =
Input.GetAxis("KickForce");
float punchForce =
Input.GetAxis("PunchForce");
float rolyPolyForce =
Input.GetAxis("RolyPolyForce");
//set drunk strength
punchStrength = Mathf.Clamp(punchStrength -
punchForce, 0, 120);
kickStrength = Mathf.Clamp(kickStrength +
kickForce, -45, 45);
rolyPolySpeed = Mathf.Clamp(rolyPolySpeed -
rolyPolyForce, -45, 45);
//update position and drunken monkey skills
transform.position = new Vector3(location.x,
drinkMeter, location.y);
transform.rotation =
Quaternion.Euler(Vector3.forward);
transform.Rotate(Vector3.up, punchStrength,
Space.Self);
transform.Rotate(Vector3.forward,
rolyPolySpeed, Space.Self);
transform.Rotate(Vector3.right,
kickStrength, Space.Self);
//apply abuse UI
label.text = talk;
}
}
话虽如此,你认出这段代码中哪一部分需要重构以适应模型(Model)?
-
变量
-
Update函数中的内容 -
Label.text = talk -
以上都不是
-
我们有一个游戏,场景设定在夜晚,在一个巷子里,燃烧的油桶周围是无家可归的人,他们用双手取暖。
其中一位技术艺术家向您提出请求,希望改变火焰发光的过程。她要求火焰从火焰底部开始为绿色,中间为蓝色,顶部为黄色。然而,这是技术艺术家第五次要求进行颜色渐变更改了!
重要提示
为了刷新您对粒子模块的知识,请查看以下链接:Unity 官方文档 - ParticleSystem.ColorOverLifetimeModule。
您的更改必须通过脚本完成(根据您的老板的要求),而不是在检查器窗口或任何形式的拖放方法中。
选择您将用于更改粒子系统颜色过程的选项:
-
创建一个颜色数据类型,并将整个桶状粒子系统的变量存储在其中作为
ColorburningFlames = flamingBarrel.ColorOverLifeTime.color。 -
将控制粒子系统颜色渐变的粒子系统模块缓存为
ParticleSystem.ColorOverLifetimeModule flameStored = flamingBarrel.colorOverLifetime;。 -
忘记那个脚本吧!使用预制体——她可以自己做到这一点!
-
这一切都是关于影响颜色的粒子速度,例如
Color burningFlames = flamingBarrel.colorBySpeed.color;。 -
有代码是为随着时间的推移饮用的水编写的。我们的协程正在检查直到没有水为止。
这里是代码的示例:
public float waterMeasurements = 10f;
private void Start()
{
StartCoroutine(CheckWaterDrank());
}
private void Update()
{
//Drinking Water
waterMeasurements -= Time.deltaTime;
}
IEnumerator CheckWaterDrank()
{
bool AnyWaterLeft()
{
return waterMeasurements <= 0;
}
//ADD NEW CODE HERE...
Debug.Log("No water left");
}
IEnumerators 需要一个返回类型。我们应该给它什么返回类型才能在正确的时间触发日志?
-
yield return new WaitUntil(AnyWaterLeft); -
yield return new WaitWhile(AnyWaterLeft); -
return null; -
yield return new WaitForSeconds(null);
其他信息
为了进一步帮助您回答这个问题,请查看 Unity 官方脚本参考页面上的以下链接:
-
您的团队已经为他们的客户开发了一个应用程序。客户审查了工作,非常喜欢,但希望对横幅和背景颜色进行一些修改。这可以很容易地实现,无需添加或更改任何额外的代码。您的团队中的艺术家已经提出应用这些新更改。
其他信息
单元测试 – 测试每个单独的代码片段(脚本/类)
集成测试 – 将单元组合在一起并确保没有引入错误
验收测试 – 客户检查代码/项目是否符合他们的要求
回归测试 – 检查是否引入了新的错误
除了一个之外,所有测试都已完成;根据提供的信息,哪个测试与客户接受这项工作相似?
-
接受测试
-
集成测试
-
单元测试
-
回归测试
-
以下代码在我们的游戏中生成吸血鬼蝙蝠。当蝙蝠进入光束时,它将被销毁。每个蝙蝠类都包含一个名为
CheckForLight()的函数,并返回一个bool。
下面是代码的一个示例:
using System.Collections.Generic;
using UnityEngine;
public class VampireBats : MonoBehaviour
{
public GameObject batPrefab;
public float spawnRate = 10f;
private float timer = 0f;
private List<Bat> bats;
private void Start()
{
bats = new List<Bat>();
}
private void Update()
{
timer += Time.deltaTime;
if (timer > spawnRate)
{
SpawnBats();
}
}
private void SpawnBats()
{
timer -= spawnRate;
GameObject spawnedBat =
Instantiate(batPrefab, transform);
Bat batToAdd =
spawnedBat.GetComponent<Bat>();
bats.Add(batToAdd);
}
public void BeamOfLight()
{
//NEW CODE TO ADD
}
}
BeamOfLight()函数将执行以下操作:
-
检查与
VampireBats脚本相关的每个蝙蝠。 -
从蝙蝠的
CheckForLight函数获取返回的bool值。 -
如果为
true,则删除蝙蝠的引用(从列表中移除)并从游戏中移除它。
哪个for循环可以解决这个问题?
for (int i = bats.Count - 1; 1 >= 0; i--)
{
if (bats[i].IsInTheLight())
{
monsters.Remove(bats[i]);
Destroy(monsters[i].gameObject);
}
}
for (int i = 0; i < bats.Count; i++)
{
if (bats[i].IsInTheLight())
{
bats.Remove(bats[i]);
Destroy(bats[i].gameObject);
}
}
for (int i = 0; i < bats.Count; i++)
{
Bat thisBat = bats[i];
if (bats[i].IsInTheLight())
{
bats.Remove(thisBat);
Destroy(thisBat.gameObject);
}
}
for (int i = bats.Count - 1; i >= 0; i--)
{
Bat thisBat = bats[i];
if (bats[i].IsInTheLight())
{
bats.Remove(thisBat);
Destroy(thisBat.gameObject);
}
}
- 以下代码在 Unity 编辑器中运行良好,但在构建中会出错。
下面是代码的一个示例:
public class CoinCounter : MonoBehaviour
{
private bool init;
#if UNITY_EDITOR
public int CoinCollection { get; private set; }
#else
CoinCollection = 1;
#endif
public void Count()
{
CoinCollection += 1;
init = true;
}
public void Reset()
{
#if UNITY_EDITOR
if (init)
{
CoinCollection = 0;
}
#endif
}
}
构建中错误的原因是什么?
-
游戏在编辑器中运行良好;可能是系统有问题。
-
代码中空格太多。
-
CoinCollection仅在UNITY_EDITOR指令中定义。 -
Reset函数没有被调用。 -
当我们保存以下代码时,控制台会生成一个错误:
using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } private List<GameObject> objectsInTheScene; public void Bucket(GameObject go) { objectsInTheScene.Add(go); } } public class AnotherDumbClass : MonoBehaviour { private void Start() { GameManager.Bucket(gameObject); } }额外提示
如果不知道答案,尝试将代码复制粘贴到空脚本中,看看是否可以尝试解决问题。
你认为错误的原因是什么?
-
需要一个对象引用来访问非静态字段、方法或属性,例如
GameManager.Bucket(GameObject)。 -
Bucket没有与 Bitbucket 连接。 -
Start不能是private。 -
没有生成错误。
-
以下代码是一个触摸屏游戏的代码,其中玩家将布丁派投向对手的脸。已经要求当玩家将手指从屏幕上移开时发射派。
以下是一个代码示例:
using UnityEngine;
public class StartGame : MonoBehaviour
{
private void Update()
{
WaitingForTouch();
}
private void WaitingForTouch()
{
if (Input.touches.Length > 0)
{
Touch touch = Input.GetTouch(0);
if (/* ENTER CODE HERE */)
{
LaunchCustardPieInTheirFace();
}
}
}
}
应该将哪段代码输入到if语句的条件中?
-
If (touch.phase == TouchPhase.Ended) -
if (ALT+CTRL+DEL) -
if (touch.phase == TouchPhase.Begin) -
if(Input.GetFinger == null) -
当场景在 Unity 编辑器中运行时,你按下键盘上的空格键,并观察到
points变量在增加时给出不一致的点数。
下面是代码的一个示例:
using UnityEngine;
public class ZoomPacer : MonoBehaviour
{
public int points;
private void FixedUpdate()
{
if (Input.GetKeyDown(KeyCode.Space))
{
points++;
}
}
}
什么可以帮助缓解这个问题?
-
将
FixedUpdate改为Update,因为Update在每一帧都会被调用以进行检查。 -
每次按下空格键时触发一个事件。
-
将
points数据类型从int改为float。 -
为每个空格键的按下设置一个时间延迟,以便在每次按下后冷却。
-
我们被要求创建一个基本原型,其中一辆车撞到墙上并弹回,使用刚体物理。
带有它们的组件的 GameObject 被检查并正确设置,但似乎代码有问题,因为汽车直接穿过墙壁而没有发生碰撞反应。
下面是代码的一个示例:
using UnityEngine;
public class Car : MonoBehaviour
{
public float wallImpactForce = 88f;
private void OnTriggerEnter(Collider other)
{
Rigidbody rb =
other.GetComponent<Rigidbody>();
rb.AddForce(Vector3.up * wallImpactForce);
}
}
导致汽车穿过墙壁的原因是什么?
-
在
AddForce参数中添加ForceMode.Impuse。 -
减少汽车的质量。
-
增加驾驶员的质量。
-
减少项目的 timestep。
-
你在一个团队中工作,团队成员技能不同,可能需要处理相同/类似的文件。在以下列表中,哪一项对团队最有帮助?
-
预制件
-
版本控制
-
离线复制文件
-
每个人都在单独的文件夹中工作
-
-
以下代码是代表系统的一部分,其中一段代码可以订阅。然而,在检查器中公开代表是不可能的。
以下是一个代码示例:
public delegate void EatFood();
public EatFood onTriggerFood;
void OnEnable()
{
onTriggerFood += Apple;
}
void OnDisable()
{
onTriggerFood -= Apple;
}
void Start()
{
onTriggerFood.Invoke();
}
void Apple()
{
Debug.Log("MUNCH");
}
什么可以作为良好的等效替代品,并在 检查器 中公开其字段?
-
在
scripts类上方添加System.Serializable。 -
用 Unity 事件系统替换代表。
-
将代表设为私有,并给它们添加
SerializeField属性。 -
这不可能完成。
-
我们正在制作一个包含多个级别的游戏。每个级别都有自己的场景;已经要求当级别改变时,内容需要作为持久数据保留在场景中。但是当游戏关闭时,所有数据都会被清除。
完成后,从一场景传递信息到另一场景并清除其内容的最简单方法是什么?
-
将一个带有所有保存数据的组件的 GameObject 添加到层次结构中。然后,向其添加
DontDestroyOnLoad()函数。 -
使用
PlayerPrefs存储所有数据,然后在游戏退出时调用PlayerPrefs.DeleteAll()。 -
使用
PlayerPrefs存储场景数据,然后在加载新关卡时,将PlayerPrefs数据复制到新的 PlayerPrefs 数据中,并删除之前关卡的人民币玩家数据。 -
复制 GameObjects,然后在加载新场景时,将存储的 GameObjects 粘贴到新场景中。
-
其中一位艺术家注意到游戏中的游泳者游泳不顺畅。
以下是一些示例代码:
public class Swimmer : MonoBehaviour
{
[SerializeField]
private int swimmingSpeed = 30;
private void FixedUpdate()
{
if (Input.GetKey(KeyCode.A))
{
transform.position += new Vector3(1 , 0 , 0) * swimmingSpeed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.D))
{
transform.position -= new Vector3(1 , 0, 0) * swimmingSpeed * Time.deltaTime;
}
}
}
你能做些什么来帮助解决这个问题?
-
将
FixedUpdate改为Update。 -
忽略艺术家;对你来说似乎没问题。
-
将两个
if语句合并为一个。 -
将两个
if语句放入一个方法中,并将其应用于FixedUpdate()。
第十五章:附录
本节将包括一些额外的注释和其他由于几个原因不适合我们的 Unity 项目的话题——例如,虚拟现实(VR)对于侧滚动游戏可能适用,但理想情况下更适合第一人称视角格式,以帮助解决开发者可能遇到的问题。
最后,在最后一个主题中,我们将涵盖一些与 Unity 相关的随机一般知识,这些知识也可能有助于你通过考试。
虚拟现实开发
如你所知,VR 自 90 年代以来就已经在商业上存在,但直到 Oculus 和 Vive 头戴式设备可以连接到 PC,才被更广泛地认识。在此之后不久,手机也被转变为 VR 头戴式设备,作为一种更经济的替代品,例如 Google Cardboard 和三星的 Gear VR 附加头戴式设备。
作为 Unity 开发者,我们不仅需要了解这些 VR 设备的技術限制,还需要了解为什么有些人会感到不适,而有些人则不会。
如果,例如,大脑和身体知道他们并没有居住在他们眼睛告诉他们的那个世界中,VR 游戏/模拟可能会很快被拒绝。
注意
如果你想要了解更多关于使用/开发 VR 应用的健康和安全方面的信息,请参考以下链接:retrophil.codes/Self-Study-App-Cognitive-Behaviour-GearVR。
因此,在性能和 VR 应用方面,帧率很重要。开发者被鼓励追求 90 帧的高帧率,以避免与用户所在世界的突然断开。延迟或更准确地说,运动到光子(MTP)不应超过 20 毫秒(用户移动头部时的更新延迟),显示刷新率为 90Hz(显示每 11 毫秒刷新一次)。
以下图表显示了在设计 VR 应用时需要实现的前述三个技术目标:

图 A.1 – 使用 VR 要实现的技术目标
因此,如果我们能持续达到这三个目标,用户将感觉更沉浸在他们的世界中。保持高性能意味着要小心使用平台资源。例如,如果,在用户的距离内,有大量的 3D 资产带有材料和各种纹理,而这些玩家永远不会到达,我们不妨用 skybox 来替换这些资产,以帮助保持 VR 应用的流畅性。另一种旨在帮助 VR 应用运行顺畅的技术是在 VR 应用中不可避免的部分改变显示纹理(renderViewportScale)。
当然,我们仍然可以通过过度使用后处理效果以及添加诸如运动模糊和景深等特性来让用户感到不适,这可能会让用户感到困惑,而移除资产上的锯齿边缘等特性则会让用户感觉更不像是身处游戏/模拟环境中。同时,即使通过超采样来提高分辨率也可能对移动设备来说是一种非常昂贵的做法。如果你的 VR 应用是基础的(没有纹理、基本光照和少量资产),你可能能够实现这一点……只要你的性能不下降!
从这个例子中我们可以看出,VR 需要在填充和抛光场景之前尽可能平稳和令人信服地运行。
游戏或考试结束建议
在 Unity 程序员考试中,你还有可能遇到关于着色器的奇怪问题,以了解你对创建着色器或不同着色器可以创建的内容的了解程度。了解着色器本身就是一个主题,而且在你参加考试时,大部分内容也不太可能专注于着色器。因此,如果你不知道如何编写着色器,不要担心编码,而应更多地关注一般实践和使用的功能。例如,可以通过使用Camera.depthTextureMode等函数来计算场景的深度,从而实现环境中的卡通着色或自定义后处理效果。
了解这些函数和方法的存在将提高你回答这些问题的机会,如果你需要有关着色器的一般信息,请参阅 Packt 出版的《Unity 2021 着色器和效果食谱 – 第三版》一书;否则,我建议浏览 Unity 提供的着色器参考手册:dev.rbcafe.com/unity/unity-5.3.3/en/Manual/SL-SurfaceShaders.html。
在下一节中,我将介绍如何在 Unity 的后续版本上安装后处理 v2。
渲染路径
Unity 程序员考试中的一些模拟问题可能会提到前向和延迟图形设置,但除此之外,这些设置除了可以在检查器窗口的相机组件中进行选择之外,还有什么?
以下截图显示了相机组件中的替代渲染路径:

图 A.2 – 渲染路径
正如我们在前面的截图中所看到的,存在多种不同的渲染路径。这些路径中的每一个都会以略微不同的方式渲染场景的表面和光照。有些可能比其他路径运行得更快,但会缺少其他好处,比如抗锯齿。
注意
有关各种渲染路径的图表比较,请参阅以下链接:docs.unity3d.com/Manual/RenderingPaths.html。
优化“杀手波浪”
因此,我们真的到了这本书的结尾,整个设计游戏/原型的过程是一种尝试,在多种场景下尽可能覆盖 Unity 程序员考试中的所有目标。在这个教程系列中,显然有些事情可以更快或做得更好,关于创建项目,但这本书从未是关于制作游戏的。它是在你的项目开发的同时尽可能覆盖更多内容。
此外,如果你只是出于制作游戏的目的购买了这本书,而你之前没有制作过游戏,你已经覆盖了一系列工具和组件,你现在可以使用它们,并且肯定可以找到作为 Unity 开发者的工作。我在 30 多个 Unity 项目中所做的大部分工作都源于我在这本书中展示的技能。因此,如果你想继续 Killer Wave 或将其重命名并更改概念以个性化游戏,那就去做吧。你有一个足够的基础来继续前进,但接下来你该把游戏做到哪里去?
以下是一个你可以继续在 Killer Wave 上工作的项目列表:
优化代码:
-
尽可能多地使用 Unity Profiler,并从其列表中移除最昂贵和第一个资源,正如在 第十三章,效果、测试、性能和替代控制 中讨论的那样。
-
随着你的游戏可能变得更大和更复杂,使用
GameObject.Find和Transform.Find等函数将使你的游戏运行得更慢。通过其他方式引用这些变量,例如在 检查器 中。 -
避免在
for循环中任何形式的if语句。 -
任何与 Vector3 和 floats 的乘法都需要单独进行(将所有 floats 放在括号内,以防止代码在变量之间来回切换)。
-
以下代码块展示了将 Vector3 和 floats 分开的示例:
transform.position = lastPos + wantedVelocity * (speed * speedFactor * Mathf.Sin(someOtherFactor) * drag * fricition * Time.deltaTime); -
缓存变换;Unity 会执行检查以确定游戏对象是否与其独立的变换一起被删除。以下代码块展示了缓存变换的示例:
Transform _transform;void Start(){_transform = this.transform;} -
如果可能,使用
transform.localPosition而不是transform.position。Unity 会自动将所有数据作为局部位置存储在内部。 -
通过缓存变量来减少引擎调用。
-
移除
get和set访问器,并将变量保持为public以避免访问。 -
尽量避免
Vector数学,并用缓存乘以 floats 来替换。这节省了创建Vector并在其中存储值。 -
将
Time.deltaTime存储为staticfloat 以避免多次引擎调用。 -
使用
for循环而不是foreach,因为foreach会创建垃圾。 -
使用
array而不是List。 -
尽量在 Profiler 中将 GC Alloc 列表保持在零。基本上,如果可能的话,不要生成垃圾以避免性能峰值。
-
在第一帧中实例化游戏对象。
-
创建子弹对象池而不是实例化和销毁。
-
不要使用
string连接;在最坏的情况下,使用StringBuilder。 -
创建自己的
Update/FixedUpdate管理器,而不是使用 Unity 提供的那些。这有助于性能,并且你可以创建自己的自定义功能来添加到其中。 -
使用动画精灵而不是 3D 资产。
-
尽量不要使用
GetComponent/GetComponentInChildren/GetComponentsInChildren/GetComponentInParent/GetComponentsInParent。如果你确实使用它们,请确保在启动时(Awake, Enable, Start)使用。 -
避免对具有大型层次结构的游戏对象进行动画处理。
-
如果场景加载时间过长,创建一个加载界面。
-
在你打算构建游戏/应用程序的平台(开发模式和/或Logcat)上进行所有最终的Profiler测试。
其他游戏想法:
-
为当前敌人创建替代的可脚本化资产。
-
在商店中提供更多商品。
-
添加不同的敌人生成器。
-
让集束炸弹做些事情!
-
与 Boss 战斗。
-
创建第 4 级。
我希望这些想法/建议能有所帮助,并且总的来说,这本书能帮助你踏上旅程。祝你好运!
模拟答案
一些章节末尾的模拟问题的答案可以在以下部分找到:
第三章,管理脚本和进行模拟测试

第七章,创建游戏循环和模拟测试

第十章,暂停游戏,调整音效,以及模拟测试


, NavMesh, 时间轴,以及模拟测试


第十四章,完整的 Unity 程序员模拟考试






更多的问题和答案可以在以下链接的 Packt 仓库中找到:https://github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition /tree/main/Extra.

订阅我们的在线数字图书馆,全面访问超过 7,000 本书和视频,以及领先的行业工具,帮助你规划个人发展并提升职业生涯。更多信息,请访问我们的网站。
第十六章:为什么订阅?
-
通过来自 4,000 多位行业专业人士的实用电子书和视频,节省学习时间,增加编码时间
-
通过为你量身定制的技能计划提高你的学习效果
-
每月免费获得一本电子书或视频
-
完全可搜索,便于轻松访问关键信息
-
复制粘贴、打印和收藏内容
你知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?你可以在 packt.com 升级到电子书版本,并且作为印刷书客户,你有权获得电子书副本的折扣。有关更多详情,请联系我们 customercare@packtpub.com。
在 www.packt.com,你还可以阅读一系列免费的技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。
你可能还会喜欢的其他书籍
如果你喜欢这本书,你可能对 Packt 的其他书籍也感兴趣:

动手实践 Unity 2021 游戏开发
Nicolas Alejandro Borromeo
ISBN: 978-1-80107-148-2
-
探索 C# 和视觉脚本工具以自定义游戏的各种方面,例如物理、游戏玩法和用户界面
-
使用 Unity 的新着色器图和通用渲染管道程序编写丰富的着色器和效果
-
通过全屏效果实现后处理,提高图形质量
-
从零开始使用 VFX Graph 和 Shuriken 为你的 Unity 游戏创建丰富的粒子系统
-
使用 Animator、Cinemachine 和 Timeline 为你的游戏添加动画
Packt 正在寻找像你这样的作者
如果你有兴趣成为 Packt 的作者,请访问 authors.packtpub.com 并今天申请。我们已与成千上万的开发者和技术专业人士合作,就像你一样,帮助他们将见解分享给全球科技社区。你可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。
分享你的想法
现在你已经完成了 Unity 认证程序员考试指南 第二版,我们非常想听听你的想法!如果你从亚马逊购买了这本书,请点击此处直接跳转到该书的亚马逊评论页面并分享你的反馈或在该购买网站上留下评论。
你的评论对我们和科技社区都非常重要,并将帮助我们确保我们提供高质量的内容。
你可能还会喜欢的其他书籍
你可能还会喜欢的其他书籍




浙公网安备 33010602011771号