原文:Developing 2D Games with Unity
一、游戏和游戏引擎
在这介绍性的一章中,我将谈一点关于游戏引擎的事情:它们是什么,以及为什么使用它们。我还将讨论几个具有历史意义的游戏引擎,并介绍 Unity 的高级功能。如果你想直接制作游戏,可以随意浏览或跳过这一章,以后再回来看。
游戏引擎——它们是什么?
游戏引擎是软件开发工具,旨在降低视频游戏开发所需的成本、复杂性和上市时间。这些软件工具在开发视频游戏的最常见任务之上创建了一个抽象层。抽象层被打包到工具中,这些工具被设计为可互操作的组件,可以直接替换或用其他第三方组件进行扩展。
游戏引擎通过减少制作游戏所需的知识深度,提供了巨大的效率优势。它们可以是预建功能最少的,也可以是全功能的,让游戏开发者可以完全专注于编写游戏代码。对于只想专注于尽可能做出最好游戏的单人开发者或团队来说,游戏引擎提供了一个超越从头开始的难以置信的优势。在构建本书中的示例游戏时,您不需要从头开始构建复杂的数学库,也不需要弄清楚如何在屏幕上呈现单个像素,因为创建 Unity 的开发人员已经为您完成了这些工作。
设计良好的现代游戏引擎在内部分离功能方面做得很好。游戏代码由描述玩家和库存的代码组成,与解压缩. mp3 文件并将其加载到内存中的代码分开保存。游戏代码将调用定义良好的引擎 API 接口来请求诸如“在这个位置绘制这个精灵”之类的事情。
一个设计良好的游戏引擎的基于组件的体系结构考虑到了鼓励采用的可扩展性,因为开发团队并不局限于一组预先确定的引擎功能。如果游戏引擎源代码不是开源的,或者许可费用非常昂贵,那么这种可扩展性就特别重要。Unity 游戏引擎是专为第三方插件而设计的。它甚至提供了一个包含插件的资产库,可以通过 Unity 编辑器访问。
许多游戏引擎也允许跨平台编译,这意味着你的游戏代码不局限于单个平台。该引擎通过不对底层计算机架构进行假设,并让开发人员指定他们使用的平台来实现这一点。如果你想发布你的游戏用于主机、桌面和移动设备,游戏引擎允许你切换几个开关来设置该平台的构建配置。
不过,对于跨平台编译的奇迹,也有一些警告。虽然跨平台编译是一项令人惊叹的功能,也证明了游戏技术的进步,但请记住,如果您正在为多个平台构建游戏,您需要提供不同的图像大小,并允许控件中的代码读取来接受不同类型的外围设备,如键盘。你可能需要调整游戏在屏幕上的布局以及许多其他任务。将一个游戏从一个平台移植到另一个平台实际上可能需要做很多工作,但是你可能不需要接触游戏引擎本身。
一些游戏引擎是如此的可视化,以至于他们允许不用写一行代码就能创建游戏。Unity 具有定制用户界面的能力,这些用户界面可以被开发团队的其他非程序员成员使用,例如关卡设计师、动画设计师、美术指导和游戏设计师。
有许多不同类型的游戏引擎,没有规则规定哪些功能是绝对需要的。最流行的游戏引擎包含以下部分或全部功能:
-
图形渲染引擎,支持 2D 或 3D 图形
-
支持碰撞检测的物理引擎
-
音频引擎加载和播放声音和音乐文件
-
实现游戏逻辑的脚本支持
-
定义游戏世界的内容和属性的世界对象模型
-
动画处理加载动画帧并播放它们
-
允许多人游戏、可下载内容和排行榜的网络代码
-
多线程允许游戏逻辑同时执行
-
内存管理,因为没有计算机有无限的内存
-
用于寻路和计算机对手的人工智能
如果你还没有完全接受使用游戏引擎,考虑下面的类比。
说你要盖房子。首先,这所房子将有一个混凝土地基,一个漂亮的木地板,坚固的墙壁,和一个风化处理的木屋顶。建造这座房子有两种方法:
建造房子的第一种方法
用手铲挖掘地面,直到你挖到足够的深度来种植地基。将石灰石和粘土放入窑中,在 2640 华氏度的温度下加热,研磨,并掺入少量石膏,制成混凝土。将你制作的粉末状混凝土,与水、碎石或细沙混合,然后打好地基。
在你打地基的同时,你需要钢筋来加固混凝土。收集制造钢筋所需的铁矿石,在高炉中熔炼,制成钢锭。将这些铝锭熔化并热轧成坚固的钢筋,用于混凝土基础。
之后,是时候搭建框架来悬挂墙壁了。拿起你的斧头,开始砍树。砍伐几百根左右的木材就足够供应原材料了,但是接下来你需要把每根木材加工成木材。完成后,别忘了处理木材,使其不受天气影响,不会腐烂或滋生昆虫。建造你的托梁和大梁,你将在上面铺设地板,你累了吗?我们才刚刚开始!
建造房子的第二种方法
购买袋装预拌混凝土、钢筋、从工厂加工的木材、一打纸带镀锌钉和气动钉枪。混合并浇注你的混凝土来建造你的地基,放下预制的钢筋,让混凝土凝固,然后用处理过的木材建造你的地板。
关于第一种方法
建造房子的第一种方法需要大量的知识,仅仅是创造建造房子所需的材料。这种方法要求你知道制造混凝土和钢材所需原材料的精确比例和技术。你需要知道如何砍伐树木而不被压在树下,你还需要知道处理木材所需的适当化学物质,你已经煞费苦心地将木材切割成数百根均匀的横梁。即使你拥有用这种方法建造房子所需的所有知识,它仍然会花费你数千个小时。
第一种方法类似于坐下来不使用游戏引擎来编写视频游戏。你必须从头开始做所有的事情:编写数学库、图形渲染代码、碰撞检测算法、网络代码、资产加载库、音频播放器代码等等。即使你一开始就知道如何做所有这些事情,你仍然需要花很长时间来编写游戏引擎代码并调试它。如果你不熟悉线性代数,渲染技术,以及如何优化剔除算法,你应该预料到,在你拥有足够的游戏引擎来实际开始编写游戏之前,你可能需要花费数年时间。
关于第二种方法
建造房子的第二种方式假设你不是完全从零开始。这并不要求你知道如何操作高炉,砍伐数百根木材,或者把它们磨成木材。第二种方法可以让你完全专注于建造房子,而不是制造建造房子所需的材料。如果你仔细选择材料并知道如何使用它们,你的房子会建造得更快,成本更低,而且质量可能更高。
第二种方法类似于坐下来编写一个视频游戏,并使用一个预建的游戏引擎。游戏开发者能够专注于游戏的内容,而不需要知道如何进行复杂的计算来判断两个物体在空中飞行时是否发生碰撞,因为游戏引擎会为他们完成这项工作。不需要构建资产加载系统、编写低级代码来读取用户输入、解压缩声音文件或解析动画文件格式。没有必要为所有视频游戏构建这种通用的功能,因为游戏引擎开发人员已经投入了数千小时来编写、测试、调试和优化代码来做这些事情。
总之…
游戏引擎给开发下一款热门游戏的独立开发者或大工作室团队带来的优势怎么强调都不为过。一些开发人员想要编写他们自己的游戏引擎作为编程练习,以了解一切是如何在引擎盖下工作的,他们将会学到大量的东西。但是如果你的目的是发布一款游戏,那么不使用预制的游戏引擎会给你自己带来伤害。
历史上的游戏引擎
历史上,游戏引擎有时与游戏本身紧密相关。1987 年,朗·吉尔伯特在芯片晨星公司的帮助下,在卢卡斯影业游戏公司工作时,为游戏引擎 Maniac Mansion 创建了脚本创建实用程序 SCUMM。SCUMM 是一个为特定的类型游戏定制的游戏引擎的很好的例子。SCUMM 中的“MM”代表狂魔大厦,这是一款广受好评的冒险游戏,也是第一款使用点击式界面的游戏,Gilbert 也发明了这款游戏。
SCUMM 游戏引擎负责将由人类可读的符号化单词组成的脚本(如“步行字符到门”)转换为字节大小的程序,以供游戏引擎解释器读取。翻译负责控制屏幕上游戏的演员,并呈现声音和图形。编写游戏而不是编码的能力,促进了快速原型制作,并允许团队从早期阶段就开始构建并专注于游戏。虽然 SCUMM 引擎是专门为狂人大厦(图 1-1 )开发的,但它也用于其他热门游戏,如全速、猴岛的秘密、印第安纳琼斯和最后的远征:图形冒险等。
图 1-1
卢卡斯影业游戏公司出品的 Maniac Mansion 使用了 SCUMM 引擎
与 Unity 等现代游戏引擎相比,SCUMM 引擎缺乏很大的灵活性,因为它是为点击式游戏定制的。然而,像 Unity 一样,SCUMM 引擎允许游戏开发者专注于游戏性,而不是不断地为每个游戏重写图形和声音代码,从而节省了大量的时间和精力。
有时候游戏引擎会对整个行业产生巨大的影响。1991 年年中,一家名为 id Software 的公司发生了行业内的巨大转变,当时 21 岁的约翰·卡马克为一款名为 Wolfenstein 3D 的游戏开发了一个 3D 游戏引擎。在此之前,3D 图形通常仅限于缓慢移动的飞行模拟游戏或简单多边形游戏,因为可用的计算机硬件太慢,无法计算和显示快节奏 3D 动作游戏所需的表面数量。卡马克能够通过使用一种叫做光线投射的图形技术来解决当前的硬件限制。这允许通过计算和显示玩家可见的表面而不是玩家周围的整个区域来快速显示 3D 环境。
这种独特的方法让卡马克与约翰·罗梅洛、设计师汤姆·霍尔和艺术家阿德里安·卡马克一起创作了一款暴力、快节奏的关于打倒纳粹的游戏,这催生了第一人称射击游戏(FPS)类型的视频游戏。id Software 将 Wolfenstein 3D 引擎授权给了其他几款游戏。迄今为止,他们已经生产了七款游戏引擎,这些引擎已经被用于一些有影响力的游戏,如雷神之锤 III 竞技场、末日重启和沃尔芬斯坦 II:新巨人。
如今,构建一个粗略的 3D FPS 游戏原型是一个有经验的游戏开发人员可以使用 Unity 这样强大的游戏引擎在几天内完成的事情。
当今的游戏引擎
现代的 AAA 游戏开发工作室,如 Bethesda 游戏工作室和暴雪娱乐公司通常有自己的内部专有游戏引擎。Bethesda 的内部游戏引擎名为:Creation Engine,用于创建上古卷轴 V:天际以及辐射 4 。暴雪有自己专有的游戏引擎,用于制作游戏,如《??》、《魔兽世界》和《守望先锋》。
专有的内部游戏引擎可能开始时是为特定的游戏项目而构建的。项目发布后,游戏引擎通常会在游戏工作室的下一个游戏中重新使用时获得新生。该引擎可能需要升级以保持最新并利用最新技术,但它不需要从头开始重建。
如果游戏开发公司没有内部引擎,他们通常会使用开源引擎,或者授权第三方引擎,如 Unity。如今,在不使用游戏引擎的情况下创建一个重要的 3D 游戏将是一项极其艰巨的任务——无论是在经济上还是在技术上。事实上,拥有内部游戏引擎的游戏工作室需要独立的编程团队,完全致力于构建引擎功能并优化它们。
说了这么多,为什么一个 AAA 工作室选择不使用像 Unity 这样的游戏引擎,而是选择建立自己的内部引擎?像 Bethesda 和 Blizzard 这样的公司有大量现成的代码可以利用,还有财政资源和大量才华横溢的程序员。对于某些类型的项目,他们希望完全控制游戏和游戏引擎的每个方面。
尽管比典型的小型游戏工作室有这些优势,Bethesda 仍然使用 Unity 来开发手机游戏:辐射避难所;而暴雪用 Unity 开发了一个小小的跨平台收藏卡牌游戏:炉石。当时间等于金钱时,像 Unity 这样的游戏引擎可以用来快速原型化、构建和迭代功能。如果你的计划是在多个平台上发布一款游戏,那么时间=金钱的等式就显得尤为重要。将内部引擎移植到 iOS 和 Android 等特定平台可能非常耗时。如果一个项目不需要你在开发像 Overwatch 这样的游戏时所需要的对游戏引擎的相同级别的控制,那么使用像 Unity 这样的交叉兼容的游戏引擎是显而易见的。
Unity 游戏引擎
Unity 是一款非常受欢迎的游戏引擎,与当今市场上的其他游戏引擎相比,它提供了大量的优势。Unity 提供了一个具有拖放功能的可视化工作流程,并支持用 C# 编写脚本,这是一种非常流行的编程语言。Unity 长期以来一直支持 3D 和 2D 图形,并且每一个版本的工具集都变得更加复杂和用户友好。
Unity 有几层许可证,对于收入高达 10 万美元的项目是免费的。它为 27 种不同的平台提供跨平台支持,并利用特定于系统架构的图形 API,包括 Direct3D、OpenGL、Vulkan、Metal 和其他几个。Unity Teams 提供基于云的项目协作和持续集成。
自 2005 年首次亮相以来,Unity 已被用于开发数以千计的桌面、移动和主机游戏和应用。这些年来 Unity 开发的一些知名游戏的小样本包括:Thomas Alone(2010)Temple Run(2011)The Room(2012)rim world(2013)炉石(2014)Kerbal Space Program(2015)pokémon GO(2014)
图 1-2
由 StudioMDHR 开发的 Cuphead 使用了 Unity 游戏引擎
对于想要定制工作流程的游戏开发者来说,Unity 提供了扩展默认可视化编辑器的能力。这种极其强大的机制允许创建定制工具、编辑器和检查器。想象一下,为你的游戏设计者创建一个可视化工具,轻松调整游戏中对象的值,如角色类别的生命值、技能树、攻击范围或物品掉落,而不必进入代码并修改值或使用外部数据库。Unity 提供的编辑器扩展功能使这一切变得可能和简单。
Unity 的另一个优势是 Unity 资产商店。资产商店是一个在线店面,艺术家、开发人员和内容创作者可以在这里上传内容进行买卖。资产商店包含数以千计的免费和付费的编辑器扩展、模型、脚本、纹理、着色器等,团队可以使用它们来加快开发进度并增强最终产品。
摘要
在这一章中,我们了解了使用预制游戏引擎比自己编写游戏引擎有很多优势。我们谈到了几个有趣的游戏引擎,以及它们对整个游戏开发的影响。我们还概述了 Unity 提供的具体优势,并提到了一些使用 Unity 引擎开发的更知名的游戏。也许很快有一天,有人会提到你的游戏,说它是用 Unity 制作的最著名的游戏之一!
二、Unity 简介
本章介绍 Unity 编辑器——安装、配置、导航其窗口、使用其工具集以及熟悉项目结构。并非所有这些材料都与您在 Unity 中的日常工作直接相关,无论如何,您将来都可能需要多次参考这一章,所以不要试图一次就记住所有内容。
安装 Unity
首先:去 https://store.unity.com
下载 Unity。因为我们只是在学习使用 Unity,获得个人版本,这是免费的。
就本书的目的而言,免费版和增强版的主要区别在于,免费版会在闪屏上显示“由 Unity 制作”,而增强版允许您创建自定义闪屏。Plus、Pro 和 Enterprise 版本逐渐变得更加昂贵,但提供了一些有趣的好处,如更好的数据分析和控制、多人游戏功能、使用 Unity 云服务的测试版本,甚至可以访问企业级的源代码。
你应该记住这些等级,你每一级的资格是由收入决定的。如果你或你的游戏公司每年的收入少于 10 万美元,你就有资格免费使用 Unity 个人版。如果您的公司年收入低于 20 万美元,您需要使用 Unity Plus 层。最后,如果您的公司年收入超过 20 万美元,您必须使用 Unity Pro。一点也不差。
安装 Unity 时,Unity 下载助手会提示您选择要安装的 Unity 编辑器组件。确保以下组件已勾选:Unity 2018(或最新版本)、文档、标准资产和示例项目。在本书中,我们将构建一个可以在你的桌面(PC、Mac 或 Linux)上独立运行的示例游戏。如果你愿意,你也可以勾选复选框来安装 WebGL、iOS 或 Android 构建支持的组件,以便为这些平台构建。
配置 Unity
安装 Unity 并首次运行后,会提示您登录您的帐户(图 2-1 )。除非你想利用一些更高级的功能,如云构建和广告,否则创建并登录一个帐户并不是真正必要的,但无论如何创建一个帐户并登录并没有什么坏处。如果您想使用 Unity 资产商店中的任何东西,您需要一个帐户。
图 2-1
Unity 登录屏幕
我们来过一遍 Unity 的项目和学习画面,如图 2-2 所示,指出几件事。在左上角,您会注意到两个选项卡——项目和学习。
图 2-2
Unity 项目和学习屏幕
选择项目,让我们浏览选项:
在磁盘上
将出现您最近参与的六个项目的历史记录,并且可以通过选择它们来打开。
在云端
这指的是使用基于云的协作项目,我们不会讨论这个。Unity Teams 有一个名为 Unity Collaborate 的功能,允许团队成员更新项目中的文件,并将这些更改发布到云中。然后,其他团队成员可以查看这些更改,并决定是将他们的本地项目与这些更改同步,还是忽略它们。如果你曾经使用过 Git,Unity Collaborate 是非常相似的,但是 Git 有一点学习曲线,Unity Collaborate 被有意设计成非常直观和易于使用。
现在选择“学习”选项卡。
学习部分有丰富的信息,您可以轻松地花几周时间浏览所有教程、示例项目、资源和链接。不要害怕打开看起来远远超出您已经知道的范围的示例项目。四处打探,调整东西,打破东西。学习就是这样发生的。如果您破坏了某些东西并且无法修复,您可以随时关闭并重新加载示例项目。
好了,让我们开始创建我们的项目。
从项目和学习屏幕的右上角选择“新建”。
您将看到一个屏幕,如图 2-3 所示,包含一些用于设置新项目的配置选项。
新 Unity 项目的默认名称是“新 Unity 项目”将项目名称改为“RPG”或“有史以来最伟大的 RPG”,如图 2-3 所示。选择“2D”旁边的单选按钮,将项目配置为始终显示 2D 的侧视图。如果您忘记设置它,也不用担心——一旦我们的项目被创建,就很容易切换。
请注意“位置”文本框中的文件路径。这就是 Unity 将拯救你的项目的地方。我喜欢把我电脑上的源代码放在一个名为“source”的父目录中,而 Unity 代码放在一个“Unity”子目录中,但是你可以随意组织你的目录结构。如果您已登录,您将看到一个切换开关来打开 Unity Analytics。您可以关闭此设置,因为我们不会使用它。
图 2-3
项目创建
点击“创建项目”按钮,用这些设置创建一个新项目,并在 Unity 编辑器中打开它。
脚本编辑器:Visual Studio
从 Unity 2018.1 开始,Visual Studio 现在是开发 C# 脚本的默认脚本编辑器。历史上,Unity 自带的内置脚本编辑器是 MonoDevelop,但从 Unity 2018.1 开始,Unity 自带了 Visual Studio for Mac,而不是 macOS 上的 MonoDevelop。在 Windows 上,Unity 附带 Visual Studio 2017 Community,不再附带 MonoDevelop。
接下来,我们将了解 Unity 编辑器。
导航 Unity 界面
横跨 Unity 编辑器顶部的是工具栏,它由转换工具集、工具手柄控制、播放、暂停和步进控制、云协作选择器、服务按钮、帐户选择器、层选择器和布局选择器组成。我们将在适当的时候讨论所有这些问题。
Unity 界面(图 2-4 )由许多窗口视图组成,我们接下来将回顾这些视图。
图 2-4
Unity 编辑器
了解不同的窗口视图
让我们浏览一下默认编辑器布局中显示的各种视图。除了我们下面讨论的视图之外,还有许多其他视图可用,我们将在本书的后面讨论其中的一些。
- 场景视图
场景可以被认为是 Unity 项目的基础,所以在 Unity 编辑器中工作时,大部分时间场景视图都是打开的。游戏中发生的一切都发生在一个场景中。场景视图是我们构建游戏和使用精灵和碰撞器完成大部分工作的地方。场景包含游戏对象,它们拥有与场景相关的所有功能。我们将在第三章中更详细地介绍游戏对象,但是现在我们只知道 Unity 场景中的每个对象都是游戏对象。
- 游戏视图
游戏视图从当前活动摄像机的视角渲染游戏。游戏视图也是你在 Unity Editor 中工作时查看和玩游戏的地方。在 Unity Editor 之外也有构建和运行游戏的方法,比如一个独立的应用,在网络浏览器中,或者在手机上,我们将在本书的后面介绍其中的一些平台。
- 资产商店
选择 Unity 构建游戏的一个引人注目的因素是 Unity 资产商店。正如在第一章中所讨论的,Unity 资产商店是一个在线店面,艺术家、开发者和内容创作者可以上传内容进行买卖。为了方便起见,Unity 编辑器有一个连接到资产商店的内置标签,但您也可以通过 Web 在 https://assetstore.unity.com
访问资产商店。虽然在你的布局中使用这个面板没有坏处,但是隐藏它并且只在你需要资产商店的东西时才打开它也没有坏处。
- 层级窗口
“层次”窗口以层次格式显示当前场景中所有对象的列表。层级窗口也允许通过左上角的“创建”下拉菜单创建新的游戏对象。搜索栏允许开发者通过名字搜索特定的游戏对象。
在 Unity 中,游戏对象可以包含其他游戏对象,这就是所谓的“亲子”关系。“层次结构”窗口将以有用的嵌套格式显示这些关系。图 2-5 描绘了示例场景中的层级窗口视图。
图 2-5
层次窗口
下面是对“层次结构”窗口中“父子”关系的简单解释。图 2-5 中的示例场景被称为 GameScene,它包含一个名为 Environment *的游戏对象。*环境是几个游戏对象的父对象,包括一个叫地面的。地面是相对于环境的子对象。然而,地面包含几个自己的子对象,包括树、灌木和道路。地面是相对于这些子对象的父对象。
- 项目窗口
“项目”窗口概述了“资源”文件夹中的所有内容。在项目窗口中创建文件夹有助于整理音频文件、素材、模型、纹理、场景和脚本等项目。在项目的整个生命周期中,您将花费大量时间拖移和重新排列文件夹中的资源,并选择这些资源以在“检查器”窗口中查看它们。在本书中,我们将演示一个建议的项目文件夹结构,但是您应该可以自由地以一种对您和您喜欢的工作方式有逻辑意义的方式重新安排事情。
- 控制台视图
控制台视图将显示 Unity 应用的错误、警告和其他输出。有一些 C# 脚本函数可用于在运行时将信息输出到控制台视图,以帮助调试。我们将在稍后讨论调试时讨论这些内容。您可以通过控制台视图右上角的三个按钮打开和关闭各种形式的输出。
小费
有时你会得到一个错误信息,每次 Unity 框架更新时都会出现,这些信息会很快堵塞你的控制台视图。在这种情况下,点击折叠切换按钮将所有相同的错误消息折叠成一条消息会很有帮助。
- 检查器窗口
检查器窗口是 Unity 编辑器中最有用和最重要的窗口之一;一定要熟悉一下。Unity 中的场景由游戏对象组成,游戏对象由脚本、网格、碰撞器和其他元素组成。您可以选择一个游戏对象,并使用检查器来查看和编辑附加的组件及其各自的属性。甚至有技术可以在游戏对象上创建你自己的属性,然后可以修改。我们将在后面的章节中详细介绍这一点。您也可以使用检查器来查看和更改预设、摄影机、材质和资源的属性。如果选择了资源,如音频文件,检查器将显示详细信息,如文件的载入方式、导入的大小和压缩率。材质贴图等资源将允许您检查渲染模式和着色器。
小费
请注意,您可以通过快捷方式访问许多更常用的面板:Control (PC)或 Cmd / ⌘ (Mac) + number。例如,⌘ + 1 和⌘ + 2 分别在 Mac 上的场景视图和游戏视图之间切换。这是一个节省时间的好方法,可以避免使用鼠标进行更常见的窗格切换。
配置和自定义布局
通过抓住窗格左上角的选项卡并拖动它,可以重新排列每个窗格。Unity 允许用户创建一个自定义的编辑器布局,方法是拖动窗格,锁定它们,根据你的喜好调整它们的大小,然后保存布局。
要保存布局,您有两种选择:
-
进入菜单选项:窗口➤布局➤保存布局。出现提示时,为您的自定义布局命名,然后点击保存按钮。
-
点击 Unity 编辑器右上角的布局选择器(图 2-6 )。一开始会说违约。然后选择保存布局,给你的自定义布局一个名字,点击保存按钮。
您可以在将来从同一个菜单加载任何布局:窗口➤布局,或使用布局选择器。如果您想重置您的布局,只需从布局选择器中选择默认。
图 2-6
布局下拉菜单
变换工具集
接下来,我们将浏览组成工具栏的不同按钮和开关。现在工具栏需要注意的三件事是:转换工具集;工具手柄控制;以及播放、暂停和步进控件。工具栏上还有其他控件,但是我们在开始使用它们的时候会用到它们。
变换工具(图 2-7 )允许用户在场景视图中导航并与游戏对象互动。
图 2-7
变换工具集
六个变换工具从左到右分别是:
- 手
手形工具允许您在屏幕上左键单击并拖动鼠标来平移视图。请注意,当选择“手形工具”时,您将无法选择任何对象。
- 移动
选择移动工具并在层级或场景视图中选择一个游戏对象将允许你在屏幕上移动该对象。
- 辐状的
旋转工具旋转选定的对象。
- 规模
缩放工具缩放选定的对象。
- 矩形
Rect 工具允许使用 2D 手柄移动和调整所选对象的大小,该手柄将出现在所选对象上。
- 移动、旋转或缩放选定的对象
该工具是移动、旋转和缩放工具的组合,合并为一组手柄。
您可以随时通过按下 Option (Mac)或 Alt (PC)来临时切换到“抓手”工具(仅在 2D 项目中),并在场景中移动。
小费
变换工具集中的六个控件分别映射到以下六个键:Q、W、E、R、T、y。使用这些热键可以在工具之间快速切换。
使用移动工具(热键:w)时,一个有用的技巧是通过按住 Control (PC)或 Cmd / ⌘ (Mac)让游戏对象捕捉到特定的增量。在“编辑➤捕捉设置”菜单中调整捕捉增量设置。
手柄位置控制
在变换工具集的右边,你会发现手柄位置控件,如图 2-8 所示。
图 2-8
手柄位置控制
控制柄是对象上的 GUI 控件,用于在场景中操纵它们。手柄位置控制允许您调整选定对象的手柄位置以及它们的方向。
第一个切换按钮(见图 2-8 )允许您设置手柄的位置。
位置的两个选项是:
-
轴:这会将控制柄放置在选定对象的轴点。
-
中心:将控制柄放置在选定对象的中心。
第二个切换按钮允许您设置手柄的方向。请注意,如果选择了缩放工具,方向按钮将灰显,因为方向与缩放无关。两个方向选项是:
-
局部:选中时,变换工具功能将与游戏对象相关。
-
全局:选中时,变换工具功能将相对于世界空间方向。
小费
通过在项目窗口中选择精灵,在检查器中将精灵模式切换到多重,然后点按精灵编辑器按钮,可以更改精灵的轴心点。点击精灵编辑器中的“切片”按钮,并从下拉菜单中选择一个轴点。
播放、暂停和步进控制
Unity 编辑器有两种模式:播放模式和编辑模式。当按下播放按钮时,如果没有错误阻止游戏构建,Unity 编辑器将进入播放模式并切换到游戏视图(见图 2-9 )。进入播放模式的快捷键是 Control (PC)或 Cmd / ⌘ (Mac) + P
图 2-9
播放、暂停和步进控制
在游戏模式下,如果你想检查正在运行的场景中的游戏对象,你可以通过选择场景面板顶部的标签切换回场景视图。如果您需要调试场景,这很有帮助。在播放模式下,您也可以随时按下暂停按钮来暂停正在运行的场景。暂停场景的快捷键在 PC 上是 Control + Shift + P,在 Mac 上是 Cmd / ⌘ (Mac) + Shift + P。
步进按钮允许 Unity 前进一帧,然后再次暂停。这也有助于调试。在 PC 上向前移动一帧的快捷方式是 Control + Alt + P,在 Mac 上是 Cmd / ⌘ (Mac) + Option + P。
在播放模式下再次按下播放按钮将停止播放场景,将 Unity 编辑器切换回编辑模式,并切换回场景视图。
在播放模式下工作时,要始终记住的一件重要事情是,一旦编辑器切换回编辑模式,您对对象所做的任何更改都不会保存或反映在场景中。当一个场景正在运行时,很容易忘记这一点,进行一些更改和调整,直到它们变得完美,只有当你停止播放时,这些更改才会丢失。
小费
为了让你在播放模式下非常明显,配置 Unity 偏好设置以在进入播放模式时自动切换编辑器的背景色调颜色是很有用的。为此,进入如图 2-10 所示的菜单选项:Unity ➤首选项。从左侧的选项中选择颜色,并查找标题为“常规”的部分选择您喜欢的背景色调颜色并退出。现在点击播放按钮查看结果。Unity 编辑器的背景应该是你选择的颜色。
图 2-10
Unity 首选项菜单
Unity 项目结构
需要了解的两个主要 Unity 项目文件夹是 Assets/ folder 和 ProjectSettings/ folder。如果您使用任何形式的源代码版本控制,这是您应该签入的两个文件夹。
资产/文件夹是所有游戏资源所在的位置,包括脚本、图像、声音文件等等。
顾名思义,ProjectSettings/文件夹包含所有类型的项目设置,包括物理、音频、网络、标签、时间、网格等等。从菜单“编辑➤项目设置”中设置的所有内容都存储在该文件夹中。
Unity 项目结构中还有其他文件夹和文件,但它们都是基于 Assets/或 ProjectSettings/的内容生成的。库/文件夹是导入资源的本地缓存,Temp/用于在构建过程中生成的临时文件。以. csproj 扩展名结尾的文件是 C# 项目文件,而以。sln 是用于 Visual Studio IDE 的解决方案文件。
Unity 文档
Unity 有很好的文档,Unity 网站上的文档( https://docs.unity3d.com/
)涵盖了脚本 API 和 Unity 编辑器的使用。Unity 在 Learn portal ( https://unity3d.com/learn
)中还有几十个视频教程,内容适合所有级别的开发者体验。Unity 论坛( https://forum.unity.com/
)是讨论 Unity 主题的地方,Unity 回答( https://answers.unity.com
)是发布问题和从社区中的 Unity 开发者伙伴那里获得帮助的重要资源。
摘要
我们已经在这一章中介绍了很多与你作为一名 Unity 游戏开发者的未来相关的内容。我们在 Unity 编辑器中介绍了最常用的窗口和视图,比如场景视图,你可以在那里构建你的游戏,游戏视图,你可以在那里查看你的游戏运行。我们讨论了层次窗口如何给出当前场景中所有游戏对象的概述,如何在检查器中编辑这些游戏对象的属性,以及如何通过变换工具集操纵它们,并处理位置控制。在此过程中,我们讨论了如何更改这些窗口和视图的布局,并保存该布局以供将来使用。我们学习了控制台视图如何显示错误信息,并在游戏出现问题时用于调试。在本章的最后,我们指出了大量的 Unity 文档、视频教程、论坛和问答资源。
三、基础
现在我们已经熟悉了 Unity 编辑器,是时候开始制作我们的游戏了。这一章将带你了解如何构造对象和编写游戏代码。我们将讨论 Unity 中使用的软件设计模式,以及计算机科学中的一些高级原则,以及它们如何与制作游戏相关。您还将学习如何在屏幕上控制播放器和播放播放器动画。
游戏对象:我们的容器实体
Unity 中的游戏是由场景组成的,一个场景中的所有东西都被称为 GameObject。在你的 Unity 冒险中,你会遇到脚本、碰撞器和其他类型的元素,所有这些都是游戏对象。将游戏对象视为一种容器是有帮助的,它由许多独立实现的功能组成。正如我们在第二章中讨论的,游戏对象甚至可以包含父子关系中的其他游戏对象。
我们将创建我们的第一个游戏对象,然后讨论为什么 Unity 使用游戏对象作为构建游戏的一个基本方面。
在层级视图中,选择左上角的创建按钮(图 3-1 ),然后选择创建空。这在层级视图中创建了一个新的游戏对象。
图 3-1
在层级视图中创建新游戏对象的一种方法
有几种不同的方法来创建游戏对象。你也可以右击等级视图面板本身,或者去顶部菜单的游戏对象➤创建空白。
右键单击新的游戏对象并选择重命名。称之为“PlayerObject”这个 PlayerObject 将包含我们 RPG 中与勇敢玩家相关的所有逻辑!
制作第二个游戏对象,并将其命名为“敌人对象”这个敌人对象将包含与我们的玩家必须击败的敌人相关的所有逻辑。
当我们学习如何在 Unity 中构建游戏时,我们还将学习计算机科学概念,这些概念将使你成为一名更好的程序员,以及这些概念将如何使你作为游戏开发者的生活更轻松。
实体组件设计
计算机科学中有一个概念叫做“关注点分离”关注点分离是一种设计原则,它描述了如何根据软件执行的功能将软件划分为模块。每个模块负责一个应该被该模块完全封装的单一功能“关注点”。当涉及到实现时,关注点可能是一个有点松散和解释性的术语——这些关注点可能广泛到在屏幕上渲染图形的责任,或者具体到计算空间中的一个三角形何时与另一个三角形重叠。
在软件设计中分离关注点的主要动机之一是减少开发人员编写重复或重叠功能时看到的浪费。例如,如果您有在屏幕上呈现图像的代码,您应该只需要编写一次该代码。一个视频游戏会有几十或几百种需要将图形渲染到屏幕上的情况,但开发者只需编写一次代码,就可以在任何地方重用它。
Unity 建立在关注点分离的哲学之上,在游戏编程中有一个非常流行的设计模式,叫做实体-组件设计。实体组件设计倾向于“组合胜于继承”,即对象或“实体”应该通过包含封装特定功能的类的实例来鼓励代码重用。实体通过这些组件类的实例获得对功能的访问。如果使用得当,组合可以减少代码,更容易理解和维护。
这不同于普通的设计方法,在普通的设计方法中,对象从父类继承功能。使用继承的一个缺点是,它会导致继承树变得又深又宽,改变父类中的一件小事会产生连锁反应,带来意想不到的后果。
在 Unity 的实体组件设计中,一个叫做游戏对象的东西就是实体,而组件实际上叫做“组件”Unity 场景中的所有东西都被认为是游戏对象,但是游戏对象本身并不做任何事情。我们在组件中实现我们所有的功能,然后将这些组件添加到我们的游戏对象中,给它们我们想要的行为。向实体添加功能和行为变得像向实体添加组件一样简单。组件本身可以被认为是不同的模块,只关注一件事,与其他关注点和代码无关。
请看下图,以更好地理解我们如何在一个假想的游戏环境中使用实体组件设计。提供行为的组件在顶部的 x 轴上,游戏中的实体在左边的 y 轴上。
| |图形渲染器
|
碰撞检测
|
物理整合
|
音频播放器
|
| — | — | — | — | — |
| 运动员 | X | X | X | X |
| 敌军 | X | X | X | X |
| 矛(武器) | X | X | X | |
| 树 | X | X | | |
| 村民 | X | X | | X |
如你所见,玩家和敌人都需要所有四个组件功能。矛武器将需要大部分功能,尤其是投掷时的物理功能,但不需要音频。这棵树不需要物理或音频——只需要图形渲染和碰撞检测来确保任何撞到它的东西都无法穿过它。上例中的村民需要图形和碰撞检测,但他们只是在场景中走动,所以他们不需要物理。如果我们希望我们的游戏播放村民与玩家互动的音轨,他们可能需要音频。
Unity 实体-组件设计并不是没有它的局限性,特别是对于大型项目来说,并且在许多年后已经开始显示出它的年龄。它将在未来被更加面向数据的设计所取代。
现在,让我们将这些新发现的知识付诸实践。
组件:构建基块
在层次视图中选择我们的 PlayerObject,注意检查器中的值是如何变化的。您应该会看到类似图 3-2 的东西。
图 3-2
变换组件
Unity 中所有游戏对象通用的一个元素是 Transform 组件,它用于确定场景中游戏对象的位置、旋转和缩放。当我们想移动我们的玩家角色时,我们将在游戏中使用转换组件。
鬼怪;雪碧
如果你是游戏开发新手,你可能会问,“什么是精灵?”视频游戏开发环境中的精灵只是 2D 的形象。如果你曾经在任天堂上看过超级马里奥兄弟(图 3-3 ),或者玩过像星谷(图 3-4 )、 Celeste、Thimbleweed Park 或者 Terraria 这样的游戏,你就玩过使用精灵的游戏。
图 3-4
在这幅星空谷的图像中,鸡、鸭、稻草人、蔬菜、树和所有其他的图像都是独立的精灵
图 3-3
超级马里奥兄弟(任天堂)中的英雄水管工马里奥的个人精灵
2D 游戏中的动画效果可以使用类似于制作动画电影、动画或卡通的技术来实现。就像卡通中的单个细胞(帧)一样,精灵会被提前显示并保存到磁盘中。以快速顺序显示单个精灵可以传达运动的印象,例如角色行走、战斗、跳跃或不可避免的死亡。
为了在屏幕上看到玩家角色,我们需要使用 Sprite 渲染器组件来显示图像。我们将把这个精灵渲染器组件添加到玩家游戏对象中。有一些不同的方法来添加一个组件到一个游戏对象,但是我们第一次将使用添加组件按钮。
从检查器中选择添加组件按钮,然后键入“sprite”并选择 Sprite 渲染器(图 3-5 )。这将组件添加到我们的玩家游戏对象中。相反,我们可以创建一个带有精灵渲染器的游戏对象,方法是转到游戏对象菜单,然后选择 2D 对象➤精灵。
图 3-5
将精灵渲染器组件添加到玩家游戏对象中
使用相同的技术将 Sprite 渲染器组件添加到 EnemyObject。
保存场景是一个需要养成的好习惯,所以让我们现在就保存场景。键入 Control (PC) / CMD (Mac) + s,然后新建一个文件夹,命名为“Scenes”。将场景保存为“LevelOne”。我们已经创建了一个新的文件夹来保存这个场景以及我们将为游戏创建的其他场景。
接下来,在项目视图中创建一个名为“Sprites”的文件夹。正如您可能已经猜到的,这将保存我们项目的所有 sprite 资产。在这个精灵文件夹下创建另一个名为“玩家”和“敌人”的文件夹。在项目视图中选择 Sprites 文件夹,然后转到下载目录中的文件夹,桌面,或者任何你放有本书下载的游戏资源的解压缩文件夹的地方。
在为第三章下载的资源中,选择名为 Player.png、EnemyWalk_1.png 和 EnemyIdle_1.png 的文件,并将它们拖到项目视图的 Sprites 文件夹中。一旦它们在主精灵文件夹中,把它们拖到各自的玩家和敌人文件夹中。你的项目视图应该类似于图 3-6 。
图 3-6
添加播放器精灵表后的项目视图。敌人的精灵在敌人文件夹里
现在在项目视图中选择播放器精灵表。注意它的属性是如何出现在右边的检查器中的。我们将在检查器中配置资产导入设置,然后使用精灵编辑器将这个精灵表分割成单独的精灵。
设置纹理类型为“精灵(2D 和用户界面)”,选择精灵模式下拉选择器,并选择“多个”这表明该子画面资产中有多个子画面。
将每单位像素更改为 32。当我们谈论相机时,我们将解释每单位像素或 PPU 设置。
将滤镜模式更改为“点(无滤镜)”这将使精灵纹理在近处看起来呈块状,这对于我们艺术作品的像素化外观来说是完美的。
在底部,按下默认按钮,选择“无”进行压缩。
再次检查检查器中的属性是否与图 3-7 相匹配。
按下“应用”按钮来应用我们的更改,然后按下检查器中的“精灵编辑器”按钮。是时候把我们的精灵表分割成精灵了。
图 3-7
播放器精灵表的属性,如检查器中所示
Unity 引擎中内置的精灵编辑器工具非常方便地获取由许多精灵组成的精灵表,并将它们分割成单独的精灵资产。
选择左上角的“切片”,并选择“按单元格大小划分网格”作为类型。这允许我们设置切片的尺寸。对于像素大小,分别为 X 和 Y 输入 32 和 32。
按下“切片”按钮。如果你仔细观察图 3-8 ,你会看到一条模糊的白线勾勒出我们每个玩家精灵的轮廓。这条白线表示 sprite 工作表被切片的位置。
图 3-8
为导入的播放器精灵表设置像素大小
现在按下“应用”按钮,将切片应用到精灵表。关闭精灵编辑器。
我们能够输入这个 sprite 工作表的精确尺寸,因为我们提前知道它们。当你在自己的游戏中工作时,你会遇到各种尺寸的精灵,你可能不得不调整一下尺寸以使它们恰到好处。Unity Sprite 编辑器还能够自动检测导入的 Sprite 表中的 sprite 尺寸,方法是在 sprite 编辑器➤切片菜单中选择“自动”。根据您使用的 sprite 工作表,这种技术可能会产生不同的结果,但这是一个起点。
那些切片和切块对我们有什么好处?单击播放器精灵表旁边的小三角形,查看从精灵表中提取的所有单个精灵(图 3-9 )。我们将使用新切割的玩家精灵创建一些动画。
图 3-9
从播放器精灵表得到的切片精灵
让我们把这些精灵的工作。选择播放器对象。在检查器视图中,一直到 Sprite 属性的右边,你会看到一个小圆圈(图 3-10 )。点击该圆圈,调出精灵选择器屏幕,如图 3-11 所示。
图 3-10
按此按钮调出选择精灵屏幕
在精灵选择器屏幕中,双击选择一个玩家精灵,当我们编辑我们的游戏时,作为我们的 PlayerObject 在场景中的替身(图 3-11 )。
图 3-11
当游戏停止时,选择一个玩家精灵来代表我们的玩家
现在我们有了所有的玩家精灵,让我们导入敌人的精灵表。选择“enemy idle _ 1”sprite 工作表,并在检查器中将其导入设置设置为与我们的 PlayerObject 相同:
-
纹理类型:精灵(2D 和用户界面)
-
精灵模式:多重
-
每单位像素:32
-
过滤器模式:点(无过滤器)
-
压缩:无
按下应用按钮。
使用 sprite 编辑器将 sprite sheet 分割成单个 32 × 32 像素的 Sprite。确保白色切片线出现在正确的位置,然后按下应用按钮并关闭精灵编辑器。对“EnemyWalk_1”精灵表执行相同的步骤,将其分割为单个精灵。
动画片
让我们创建一个新文件夹来保存我们将要创建的动画。你还记得怎么做吧?从项目视图中选择资产,右键单击,然后选择创建➤文件夹。或者,您可以单击项目视图左上角的“创建”按钮。把这个文件夹叫做“动画”。选择动画文件夹,并在其中创建另外两个子文件夹,标题为“动画”和“控制器”。
在项目视图中单击播放器精灵旁边的小箭头,展开播放器精灵。选择第一个玩家精灵—这应该是向东走的玩家的精灵。按住 shift 键选择它旁边的三个精灵。将这四个精灵一起拖动到 PlayerObject 上,如图 3-12 所示。
图 3-12
将精灵拖动到 PlayerObject 上以创建新的动画
将出现一个屏幕,提示您创建新的动画(图 3-13 )。导航到动画➤动画子目录,我们以前创建的,并保存这个动画为“播放器-步行-东”。
图 3-13
创建并保存新的动画对象
现在选择 PlayerObject 并查看检查器视图。注意我们有两个新组件(图 3-14 ):精灵渲染器和动画器。
精灵渲染器组件负责显示或渲染精灵。Unity 还添加了一个 Animator 组件,它包含一个 Animator 控制器,允许播放动画。
图 3-14
自动添加了两个新组件:精灵渲染器和动画器
将精灵拖动到 PlayerObject 并创建新的动画导致这两个组件被添加到 PlayerObject。
当我们向 PlayerObject 添加动画时,Unity 编辑器足够聪明,知道我们需要某种方式来播放和控制动画。于是它自动创建了一个 Animator 组件来播放动画,并附加了一个动画控制器对象“PlayerObject”。我们也可以按下检查器中的 Add Component 按钮,搜索“Animator”,然后手动添加一个 Animator。
名为“PlayerObject”的动画控制器将默认出现在我们保存“player-walk-east”动画的文件夹中。动画控制器的默认名称是“PlayerObject”(图 3-15 ),这很容易混淆,因为我们的主玩家游戏对象也被称为“PlayerObject”。
图 3-15
自动创建的动画控制器:PlayerObject,以及我们的第一个动画对象:player-walk-east
让我们将动画控制器重命名为更具描述性的名称。选择 PlayerObject,按 Enter 键,或右键单击,并将该对象重命名为“PlayerController”。
选择、拖动 PlayerController 对象,并将其移动到我们创建的控制器文件夹中。
双击 PlayerController 对象打开 Animator 窗口。
动画师状态机
动画控制器维护一组称为状态机的规则,用于根据玩家所处的状态来确定为关联对象播放哪个动画剪辑。玩家对象使用的状态的一些例子可能是:行走、攻击、空闲、进食和死亡。我们进一步将这些状态划分为方向,因为当我们的玩家在这些状态时,他们可能面向北、南、东或西。这些状态的可视化流程图显示在 Animator 窗口中,如图 3-16 所示。
图 3-16
动画窗口
将动画控制器视为控制动画的“大脑”是很有帮助的。动画状态机中的每个状态都由一个附加到它的动画对象表示。该动画对象包含为该状态播放的实际动画剪辑。动画控制器还维护如何在动画状态之间转换的细节。
正如您在 Animator 窗口中看到的,我们的动画控制器有以下状态:进入状态、任意、退出和我们刚刚添加的状态:玩家-行走-东。当你想转换到一个状态时,使用“任何状态”,例如从任何其它状态“跳转”。
如果您没有看到退出状态,您可能需要稍微滚动窗口来找到它。您也可以使用鼠标或触控板上的滚动按钮来放大和缩小,以便更好地查看事物,并在拖移背景时按住 Option / Alt 键,以便在 Animator 窗口中移动。在任何时候,你都可以随意移动这些动画对象,并以你认为有意义的方式排列它们。
让我们添加其余的动画。回到精灵文件夹,选择接下来的四个精灵。这些是玩家向西行走时使用的精灵。将这四个拖动到 PlayerObject 上,就像我们创建之前的行走动画一样。当创建新的动画保存窗口提示时,键入“球员-步行-西方”并保存到动画➤动画文件夹。你应该看到这个新的动画出现在动画窗口。
按照相同的步骤为其他精灵创建新的动画。请注意,“向南走”和“向北走”动画只有两帧,而不是四帧。将他们的动画命名为“玩家走南”和“玩家走北”,并将它们保存到动画➤动画文件夹中。
此时,你的动画窗口应该类似于图 3-17 所示的四个动画对象。这四个动画对象代表四种不同的行走状态,并且也包含对动画剪辑的引用。
图 3-17
将所有四个玩家行走动画添加到 PlayerObject 后,显示这些动画的 Animator 窗口
我们已经做了所有这些工作,但我们仍然没有任何东西在屏幕上动画。还有最后一步——在层次视图中,选择主相机游戏对象,并将大小属性设置为 1。这是暂时的,所以你可以清楚地看到球员动画。我们将在本书后面解释更多关于相机的内容。
按下工具栏中的播放按钮。如果一切顺利,你会看到我们无畏的玩家在原地疯狂奔跑,如图 3-18 所示。
图 3-18
我们尝到了像素化胜利的甜蜜滋味
让我们让疯狂的玩家慢下来。通过双击 PlayerObject Animator 或选择 Animator 窗口选项卡打开 Animator 窗口。选择“player-walk-east”动画,并将速度值更改为 0.6,如图 3-19 所示。
图 3-19
更改动画速度
然后再次按下 play,观看她以更可持续的速度行走。你可以把这个速度调整到任何你觉得看起来自然的速度。
再次按下播放按钮,停止播放场景。
现在为我们的 EnemyWalk_1 和 EnemyIdle_1 动画创建并保存动画。每个动画都包含五个精灵。命名动画:敌人-步行-1,和敌人-闲置-1。将 EnemyObject 动画控制器重命名为 EnemyController,并将其移动到“动画➤控制器”子文件夹。移动敌人动画到动画➤动画子文件夹。
煤矿工人
接下来我们要学习对撞机。碰撞器被添加到游戏对象中,并被 Unity 物理引擎用来确定两个对象之间何时发生碰撞。碰撞器的形状是可调的,它们的形状通常或多或少像它们所代表的物体的轮廓。有时,勾画出一个物体的精确形状在计算上是不允许的,而且通常是不必要的,因为一个物体形状的近似值对于碰撞目的来说已经足够了,并且玩家在运行时无法区分。使用一种称为“原始碰撞器”的碰撞器来近似物体形状也不需要太多处理器。在 Unity 2D 有两种类型的原始对撞机:2D 箱式对撞机和 2D 圆形对撞机。
选择 PlayerObject,然后在检查器中选择“添加组件”按钮。搜索并选择“2D 箱式对撞机”,将 2D 箱式对撞机添加到 PlayerObject 中,如图 3-20 所示。
图 3-20
将长方体碰撞器 2D 添加到 PlayerObject
我们需要知道玩家何时与敌人碰撞,所以也给敌人添加一个 2D 碰撞器。
刚体组件
添加到游戏对象的刚体组件允许游戏对象与 Unity 物理引擎交互。这就是 Unity 知道如何对游戏对象施加重力之类的力。刚体也允许你通过脚本对游戏对象施加力。例如,你的游戏可能有一个名为“汽车”的游戏对象,它包含一个刚体。你可以对汽车物体施加一定的力,使其向当前方向移动,这取决于玩家按下的按钮:油门或涡轮。
选择 PlayerObject,单击检查器中的“添加组件”按钮,搜索“刚体 2D”,并将其添加到 PlayerObject。在刚体组件的 Body Type 下拉列表中,选择“ Dynamic ”动态刚体将与其他物体相互作用和碰撞。将刚体 2D 的以下属性设置为 0:线性阻力、角阻力和重力比例。将质量设定为 1。
下拉菜单中的第二种体型是运动学。运动学刚体 2D 组件不受重力等外部物理力的影响。它们确实有速度,但只有当我们移动它们的变换组件时才会移动,通常是通过脚本。这是一种不同于我们之前描述的通过施加力来移动游戏对象的方法。第三种体型是静态,针对游戏中根本不会移动的物体。
选择 EnemyObject,并添加一个动力学类型的刚体 2D 组件。
现在我们已经为玩家和敌人添加了刚体 2D,他们将受到重力的影响。因为我们的游戏使用自上而下的视角,所以让我们关闭重力,这样我们的玩家就不会飞出屏幕。转到编辑➤项目设置➤物理 2D,并将重力 y 的值从–9.81 更改为 0。
标签和层
标签
标签允许我们在游戏运行时标记游戏对象,以便于参考和比较。
选择播放器对象。在检查器左上方的标签下拉菜单下,选择播放器标签,为我们的 PlayerObject 添加一个标签,如图 3-21 所示。
图 3-21
在检查器中选择播放器标签,将其分配给我们的 PlayerObject
玩家标签是 Unity 中每个场景的默认标签,但是你也可以根据需要添加标签。
创建一个名为“敌人”的新标签,并使用它来设置 EnemyObject 标签。随着游戏的发展,我们将为其他物品添加标签。
层
层用于定义游戏对象的集合。这些集合用于冲突检测,以确定哪些层相互了解,从而可以进行交互。然后,我们可以在脚本中创建逻辑,以确定当两个游戏对象发生冲突时该做什么。正如我们在图 3-22 中看到的,我们想要创建一个新的“用户层”叫做“阻塞”。在用户第 8 层字段中键入“阻塞”。
选择层下拉菜单,然后选择“添加层”你应该会看到图层窗口如图 3-22 所示。
图 3-22
“层”窗口
现在再次选择 PlayerObject 以在检查器中查看其属性。从下拉菜单中选择我们刚刚创建的阻塞层(见图 3-23 )将我们的 PlayerObject 添加到该层。选择 EnemyObject,并在检查器中将图层设置为“阻挡”。
图 3-23
从下拉菜单中选择阻挡层
稍后,我们将配置我们的游戏,以强制某些游戏对象不能通过阻挡层中的任何对象。例如,玩家将会在阻挡层中,任何墙、树或敌人也是如此。敌人不能穿过玩家,玩家也不能穿过任何墙壁、树木或敌人。
排序层
现在让我们看一个不同类型的层:排序层。排序层不同于常规层,因为它们允许我们告诉 Unity 引擎我们在屏幕上的各种 2D 精灵应该以什么顺序“渲染”或绘制。因为排序层与渲染相关,所以您总是会在渲染器组件中看到排序层下拉菜单。
为了更好地理解我们所说的精灵渲染的“顺序”,请看一下点击式冒险树莓公园图 3-24 中的截图。截图显示两个玩家角色站在一个房间里。我们可以在房间里看到各种各样的家具,如文件柜和桌子。在树莓公园的截图中,女侦探雷探员似乎正站在文件柜前。这个效果是在游戏引擎渲染文件柜之后,通过渲染代理人雷的精灵来完成的。
图 3-24
Thimbleweed Park 的截图,显示人物站在对象前面
Thimbleweed Park 使用自己专有的游戏引擎,而不是 Unity,但所有引擎都必须有某种逻辑来描述渲染像素的顺序。
在我们的 RPG 游戏中,我们将从上往下看,也就是所谓的“正交”视角。当我们谈到相机时,我们会更多地讨论这意味着什么,但现在我们知道我们希望 Unity 首先为地面绘制像素,然后是地面上的任何角色,如玩家或敌人,所以这些角色看起来像是在地面上行走。
我们将添加一个名为“角色”的分类层,我们将为我们的玩家和所有的敌人使用。
在检查器的精灵渲染器组件中,选择排序层下拉菜单,选择“添加排序层”,如图 3-25 所示。我们创建的排序层将在整个游戏中可用,即使我们是从 PlayerObject 的菜单中创建的。
图 3-25
添加排序层
添加一个名为“Characters”的排序层(图 3-26 ,然后再次点击 PlayerObject 查看其检查器,并从排序层下拉菜单中选择我们新的 Characters 排序层,如图 3-27 所示。
图 3-27
在我们的 PlayerObject 中使用新的字符排序层
图 3-26
添加一个名为 Characters 的新排序层
选择我们的 EnemyObject 并设置它的排序层为 Characters,因为我们希望敌人也能被渲染在地面瓷砖上。
简介:预设
Unity 允许你构建嵌入组件的游戏对象,然后用这个游戏对象创建一个叫做“预置”的东西。预设可以被认为是预先制作的模板,你可以从中创建或“实例化”已经制作好的游戏对象的新副本。这个资源有一个非常有用的特性,允许你通过改变预设模板来一次编辑所有的预设。另一方面,你可以选择改变一个单独的预置,让其余的和原来的一样。
例如,想象一下,如果你有一个玩家在酒馆里的场景。酒馆里有许多道具,如椅子、桌子和啤酒杯。如果你为所有这些道具创建了单独的游戏对象,它们中的每一个都可以独立编辑。如果您想要更改每个表的某个属性,例如,将表的颜色从浅色改为深色,您必须选择并编辑每个表,然后更改该属性。如果表对象是预置实例,你只需要改变单个对象——预置——的属性,然后点击按钮,将改变应用到从预置派生的所有实例。
我们将在构建游戏的过程中不断使用这种简单的预置技术。
从游戏对象中创建一个预置真的很容易。首先,在项目视图的资产文件夹下创建一个预置文件夹。然后从层次视图中选择我们的 PlayerObject,并简单地把它拖到我们的 Prefabs 文件夹中。
图 3-28 中的截图显示了我们将 PlayerObject 放入 Prefabs 文件夹后的预置。
图 3-28
通过拖动任何游戏对象到预设文件夹来创建一个预设
看一下图 3-28 中的层级视图。您会注意到 PlayerObject 文本是浅蓝色的。这表明 PlayerObject 是基于预设的。这也意味着接下来,如果你对玩家对象预设做了任何更改,并且你想将更改应用到预设的所有实例,你需要在项目视图中选择游戏对象时按下检查器中的应用按钮(见图 3-29 )。
图 3-29
按下“应用”按钮,将对玩家对象所做的任何更改应用到预设的所有实例
您现在可以安全地从层次视图中删除 PlayerObject,因为我们现在有了一个预置的 PlayerObject,我们可以随时使用它来重新创建 PlayerObject。如果你想编辑预设的所有实例,只需将预设对象拖回层次视图并进行更改,然后按应用。
对 EnemyObject 执行相同的操作:将其拖动到 Prefabs 文件夹中,并从层次视图中删除原始的 EnemyObject。
现在是再次保存我们的场景的好时机,所以一定要这样做。
脚本:组件的逻辑
所以我们有玩家对象和敌人对象。让他们动起来。选择我们的 PlayerObject 预置,并将其拖入层次视图。您会注意到,检查器再次填充了 PlayerObject 的属性。
滚动到检查器的底部,然后按下“添加组件”按钮。输入单词, script ,选择“New Script”。将新脚本命名为“MovementController”,如图 3-30 所示。
图 3-30
将新脚本命名为:“MovementController”
在项目视图中创建一个名为“脚本”的新文件夹。新脚本将会在项目视图的顶层 Assets 文件夹中创建。将 MovementController 脚本拖到 Scripts 文件夹中,然后双击它以在 Visual Studio 中打开它。
是时候编写我们的第一个脚本了。Unity 中的脚本是用一种叫做 C# 的语言编写的。一旦你在 Visual Studio 中打开了我们的 MovementController 脚本,它应该类似于图 3-31 。
图 3-31
Visual Studio 中的 MovementController 脚本
注意
直到最近,Unity 才允许开发人员用两种不同的语言编写脚本:C# 和一种类似 JavaScript 的语言“UnityScript”。从 Unity 2017.2 测试版开始,Unity 开始了贬低 UnityScript 的过程,但你可能会在外面找到一些 UnityScript 样本。接下来,你应该只用 C# 来为 Unity 写脚本。你可以在 Unity 的博客中了解更多关于弃用的原因:blogs.unity3d.com
。
让我们来看看典型的 Unity 脚本的结构。接下来的所有行都应该完全按照你看到的样子输入,并且 C# 中的每一行都应该以分号结束。编程语言非常字面化,不喜欢省略分号、回车或额外的字母或数字。以//开头的行是注释,只是为了澄清而写的,您不必键入它们。C# 中的注释可以用两个正斜杠://或用一个:/后跟您的注释,并以:/结束
// 1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 2
public class MovementController : MonoBehaviour
{
// 3
// Use this for initialization
void Start()
{
}
// 4
// Update is called once per frame
void Update()
{
}
}
下面是前面每个部分的分类:
// 1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
命名空间用于组织和控制 C# 项目中的类的范围,以避免冲突,并使开发人员的生活更加轻松。使用的关键字用于描述。NET Framework,并省去开发人员每次使用该命名空间中的方法时都必须键入完全限定名的麻烦。
例如,如果我们包括系统命名空间,如下例所示:
using System;
而不必输入繁琐的:
System.Console.WriteLine("Greatest RPG Ever!");
我们可以简单地输入较短的版本:
Console.WriteLine("Greatest RPG Ever!");
这是可能的,因为:使用系统;声明阐明了该类文件中的代码将使用 System 命名空间。
C# 中的命名空间也是可嵌套的。这意味着您可以在名称空间中引用名称空间,就像在系统中引用集合一样。这是这样写的:
using System.Collections;
UnityEngine 名称空间包含许多特定于 Unity 的类,其中一些我们已经在场景中使用过,比如 MonoBehaviour、GameObject、Rigidbody2D 和 BoxCollider2D。通过声明 UnityEngine 名称空间,我们可以在 C# 脚本中引用和使用这些类。
// 2
public class MovementController : MonoBehaviour
对于作为组件附加到场景中游戏对象的类,它需要从 UnityEngine 类monobehavior继承。通过从 MonoBehaviour 继承,一个类可以访问诸如 Awake()、Start()、Update()、LateUpdate()和 OnCollisionEnter()之类的方法,并保证这些方法将在 Unity 事件函数执行周期的某个点被调用。
// 3
void Start()
父 MonoBehaviour 类提供的方法之一是 Start()。我们稍后将描述事件函数的执行周期,但是正如您从它的名字可以想象的那样,Start()函数是脚本执行时首先调用的方法之一。如果满足一些条件,则在第一次帧更新之前调用 Start()方法:
-
该脚本必须从 MonoBehaviour 继承。我们的 MovementController 确实继承了 MonoBehaviour。
-
该脚本必须在初始化时启用。默认情况下,脚本将被启用,但脚本在初始化时可能未被启用,这可能是一个错误源。
// 4
void Update()
Update()方法每帧调用一次,用于更新游戏行为。因为 Update()每帧调用一次,所以一个每秒 24 帧的游戏每秒将调用 Update() 24 次,但是更新调用之间的时间可能不同。如果您需要方法调用之间的时间一致,那么使用 FixedUpdate()方法。
既然我们已经熟悉了默认的 MonoBehaviour 脚本,请用下面的代码替换 MovementController 类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MovementController : MonoBehaviour
{
//1
public float movementSpeed = 3.0f;
// 2
Vector2 movement = new Vector2();
// 3
Rigidbody2D rb2D;
private void Start()
{
// 4
rb2D = GetComponent<Rigidbody2D>();
}
private void Update()
{
// Keep this empty for now
}
// 5
void FixedUpdate()
{
// 6
movement.x = Input.GetAxisRaw(“Horizontal”);
movement.y = Input.GetAxisRaw(“Vertical”);
// 7
movement.Normalize();
// 8
rb2D.velocity = movement * movementSpeed;
}
}
// 1
public float movementSpeed = 3.0f;
声明一个公共浮动,我们将使用它来调整和设置角色的移动速度。通过将它声明为 public,我们允许这个变量 movementSpeed 在它所连接的游戏对象被选中时出现在检查器中。
看一下图 3-32 看看公共变量是如何出现在运动控制器(脚本)部分的检查器中的。Unity 会自动将公共变量的首字母大写,并在首个大写字母前添加一个空格。这意味着“运动速度”将在检查器中显示为“运动速度”。
图 3-32
公共变量 movementSpeed 以大写形式出现,并带有一个空格
// 2
Vector2 movement = new Vector2();
Vector2 是一种内置的数据结构,用于保存 2D 矢量或点。我们将使用它来表示玩家或敌方角色在 2D 空间中的位置或角色移动的方向。
// 3
Rigidbody2D rb2D;
声明一个变量来保存对 Rigidbody2D 的引用。
// 4
rb2D = GetComponent<Rigidbody2D>();
方法 GetComponent 接受一个类型的参数,并将返回附加到该类型的当前对象的组件(如果附加了一个的话)。我们调用 GetComponent 来获取我们在 Unity 编辑器中附加到 PlayerObject 的 Rigidbody2D 组件的引用。我们将使用这个组件来移动玩家。
// 5
FixedUpdate()
正如我们在前面几页所讨论的,Unity 引擎以固定的时间间隔调用 FixedUpdate()。这与每帧调用一次的 Update()方法形成对比。在较慢的硬件设备上,游戏帧速率可能会降低,在这种情况下,Update()的调用频率可能会降低。
// 6
movement.x = Input.GetAxisRaw("Horizontal");
movement.y = Input.GetAxisRaw("Vertical");
Input 类为我们提供了几种捕捉用户输入的方法。我们使用 GetAxisRaw()方法捕获用户输入,并将这些值分配给 Vector2 结构的 x 和 y 值。GetAxisRaw()方法接受一个参数,该参数指定我们感兴趣的 2D 轴是水平的还是垂直的,并从 Unity 输入管理器中检索-1、0 或 1 并返回它。
“1”表示按下了右键或“d”(使用常见的 w、a、s、d 输入配置),而“-1”表示按下了左键或“a”。“0”表示没有按下任何键。这个输入键映射可以通过 Unity 输入管理器配置,我们将在后面解释。
// 7
movement.Normalize();
这将使我们的向量“正常化”,并使玩家以相同的速度移动,无论他们是斜向移动、垂直移动还是水平移动。
// 8
rb2D.velocity = movement * movementSpeed;
将 movementSpeed 乘以运动向量将设置附加到 PlayerObject 的刚体 2D 的速度并移动它。
回到 Unity 编辑器,确保你在层次视图中看到我们的 PlayerObject。如果没有,将 PlayerObject 从 Prefabs 文件夹拖动到层次视图中。
还有最后一个非常重要的步骤:我们需要将脚本添加到 PlayerObject 中。
要将脚本添加到我们的 PlayerObject,请将 MovementController 脚本从 Scripts 文件夹拖到层次视图中的 PlayerObject 上,或者在选择 PlayerObject 时将其拖到检查器中。这就是我们如何在 Unity 编辑器中将脚本附加到对象上。当 MovementController 脚本附加到特定对象时,它可以访问 PlayerObject 中的其他组件。
现在按播放键。你应该看到我们的玩家角色在原地行走。按下键盘上的箭头键或 W、A、S、D,看着她四处移动。
恭喜你!你刚刚给曾经只是电子脉冲的东西注入了生命。你知道他们怎么说强大的力量会带来什么吗…
状态和动画
更多状态机
现在我们知道了如何在屏幕上移动我们的角色,我们将讨论如何基于当前玩家状态在动画之间切换。
转到“动画➤控制器”文件夹,双击 PlayerController 对象。您应该看到 Animator 窗口,显示我们之前设置的状态机。正如我们之前讨论过的,Unity 的动画状态机允许我们查看所有不同的玩家状态和他们相关的动画片段。
单击并拖动您的动画状态对象,直到它类似于图 3-33 中的屏幕,玩家空闲关闭到一边,玩家行走动画组合在一起。当排列它们时,不需要太精确,因为唯一真正重要的是动画状态对象之间的方向箭头。
图 3-33
Animator 窗口中动画的组织
在图 3-33 中,你可以看到玩家向东走的动画状态是橙色的。橙色表示这是这个动画的默认状态。选择然后右键单击“玩家空闲”动画状态,选择“设为图层默认状态”,如图 3-34 所示。颜色应该变成橙色。
图 3-34
右键单击并选择“设置为层默认状态”,将播放器空闲动画设置为默认动画
我们希望 player-idle 成为默认状态,因为当我们没有触摸方向键时,我们希望玩家在空闲状态下面朝南。这将看起来好像玩家角色正在等待用户。
现在选择并右键单击“任何状态”,然后选择“进行转换”将出现一条带箭头的线,连接到鼠标并围绕鼠标。点击“玩家向东走”来创建任意状态对象和玩家向东走之间的过渡。
如果你做的正确,它看起来应该如图 3-35 所示。
图 3-35
创建一个从任何状态到玩家东行的过渡
现在对其余的动画状态执行相同的操作:右键单击任意状态,创建过渡,并选择每个动画状态来创建过渡。正如我们前面提到的,当你想转换到一个状态时,使用“任何状态”,比如从任何其他状态“跳转”。
您应该创建总共五个白色过渡箭头,从任意状态指向所有四个玩家行走动画状态和玩家空闲动画状态。还应该有一个橙色的默认状态箭头,从进入动画状态指向玩家空闲动画状态,如图 3-36 所示。
图 3-36
创建从任何状态到所有动画状态的转换
动画参数
为了使用这些转换和状态,我们想要创建一个动画参数。动画参数是在动画控制器中定义的变量,由脚本用来控制动画状态机。
我们将使用我们在过渡和 MovementController 脚本中创建的动画参数来控制 PlayerObject,并让她在屏幕上走动。
选择 Animator 窗口左侧的参数选项卡(图 3-37 )。按下加号并从下拉菜单中选择“Int”(图 3-38 )。将创建的动画参数重命名为“AnimationState”(图 3-39 )。
图 3-39
将动画参数命名为:AnimationState
图 3-38
从下拉菜单中选择 Int
图 3-37
动画窗口中的“参数”标签
我们将设置动画参数在每个过渡到一个特定的条件。如果在游戏过程中这个条件为真,那么动画师将转换到那个动画状态,相应的动画片段将会播放。因为此 Animator 组件附加到 PlayerObject,所以动画剪辑将显示在场景中变换组件的位置。我们使用一个脚本将这个动画参数条件设置为真,并触发状态转换。
选择将任何州连接到 player-walk-east 州的白色过渡线。在检查器中,更改设置,使其与图 3-40 相匹配。
图 3-40
在检查器中配置转场
我们希望取消选中诸如退出时间、固定持续时间和可以过渡到自我等框。确保将过渡持续时间(%)设置为 0,并将中断源设置为“当前状态,然后是下一个状态”
取消选中“退出时间”,因为我们希望在用户按下不同的键时中断动画。如果我们选择了退出时间,那么在下一个动画开始之前,动画必须播放到退出时间框中输入的百分比,这将导致玩家体验不佳。
在检查器的底部,您会看到一个标题为“条件”的区域点击右下角的加号,选择 AnimationState,Equals,输入 1(图 3-41 )。我们刚刚创建了一个条件,它说:如果名为“AnimationState”的动画参数等于 1,那么进入这个动画状态并播放动画。这就是我们如何从将要编写的脚本中触发状态变化。
图 3-41
设置动画参数的条件:AnimationState
注意
很容易不小心将 AnimationState 下拉框中的“Greater”改为“Equals ”,所以要小心这一点。如果不将条件设置为 Equals,我们的转换将无法正常工作。
我们要做的下一件事是在脚本中将 AnimationState 参数设置为 1。回到 Visual Studio 和我们的 MovementController.cs 脚本。
将 MovementController 类替换为:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MovementController : MonoBehaviour
{
public float movementSpeed = 3.0f;
Vector2 movement = new Vector2();
// 1
Animator animator;
// 2
string animationState = "AnimationState";
Rigidbody2D rb2D;
// 3
enum CharStates
{
walkEast = 1,
walkSouth = 2,
walkWest = 3,
walkNorth = 4,
idleSouth = 5
}
private void Start()
{
// 4
animator = GetComponent<Animator>();
rb2D = GetComponent<Rigidbody2D>();
}
private void Update()
{
// 5
UpdateState();
}
void FixedUpdate()
{
// 6
MoveCharacter();
}
private void MoveCharacter()
{
movement.x = Input.GetAxisRaw("Horizontal");
movement.y = Input.GetAxisRaw("Vertical");
movement.Normalize();
rb2D.velocity = movement * movementSpeed;
}
private void UpdateState()
{
// 7
if (movement.x > 0)
{
animator.SetInteger(animationState, (int)CharStates.walkEast);
}
else if (movement.x < 0)
{
animator.SetInteger(animationState, (int)CharStates.walkWest);
}
else if (movement.y > 0)
{
animator.SetInteger(animationState, (int)CharStates.walkNorth);
}
else if (movement.y < 0)
{
animator.SetInteger(animationState, (int)CharStates.walkSouth);
}
else
{
animator.SetInteger(animationState, (int)CharStates.idleSouth);
}
}
}
// 1
Animator animator;
我们创建了一个名为“animator”的变量,稍后我们将使用它来存储一个对游戏对象中 Animator 组件的引用,这个脚本是附加到这个对象上的。
// 2
string animationState = "AnimationState";
将一个字符串直接输入到将要使用它的代码中称为“硬编码”值。当不可避免的输入错误发生时,这也是一个常见的错误来源,所以让我们通过只输入一次来避免这种可能性,然后在需要引用字符串时使用变量。
// 3
enum CharStates
数据类型“enum”用于声明一组枚举常量。每个枚举常量对应于一个基础类型值,如 int (integer),您可以引用枚举来获取相应的值。
这里我们声明了一个名为 CharStates 的枚举,并使用它来映射角色的各种状态(向东走,向南走,等等)。)以及相应的 int。我们将很快使用这个 int 值来设置我们的动画状态。
// 4
animator = GetComponent<Animator>();
在这个脚本附加的游戏对象中获取一个动画组件的引用。我们希望保存这个组件引用,这样我们以后就可以通过这个变量快速访问它,而不必每次需要时都检索它。使用 GetComponent 是从脚本中访问其他组件的最常见方式。您甚至可以用它来访问其他脚本。
// 5
UpdateState();
调用我们编写的方法来更新动画状态机。我们将这种逻辑移到了一个单独的方法中,以保持代码库的整洁和易读性。单个方法中的代码越多,就越难阅读。越难阅读的代码越难调试、测试和维护。
// 6
MoveCharacter();
我们已经移动了代码,将播放器移动到另一个方法中,以保持代码的整洁和可读性。
// 7
这一系列 if-else-if 语句将决定我们对Input.GetAxisRaw()
的调用是返回-1、0 还是 1,并相应地移动字符。
例如:
if (movement.x > 0)
{
animator.SetInteger(animationState, (int)CharStates.walkEast);
}
如果沿 x 轴的移动大于 0,则玩家按下向右键。
我们想告诉 Animator 对象应该将状态改为 walk-east,所以我们调用SetInteger()
方法来设置我们之前创建的动画参数的值,并触发状态的转换。
SetInteger()
接受两个参数:一个字符串和一个 int 值。第一个值是我们之前在 Unity 编辑器中创建的动画参数(图 3-42 )
图 3-42
我们从脚本中设置这个动画参数
我们在脚本中将这个动画参数的名称方便地存储在一个名为“animationState”的字符串中,并将它作为第一个参数传递给SetInteger()
。
SetInteger()
的第二个参数是为 AnimationState 设置的实际值。因为 CharStates 枚举中的每个值都对应一个 int 值,所以当我们键入:
CharStates.walkEast
我们实际上使用了 walkEast 在 enum 中对应的任何值。在这种情况下,walkEast 对应于 1。我们仍然需要通过将(int)写到变量的左边来将它显式地转换为一个 int。我们需要转换枚举的原因超出了本书的范围,但是与 C# 语言在幕后实现的方式有关。
保存您的脚本并切换回 Unity 编辑器,这样我们就可以使用所有这些了。选择指向 player-walk-south 的白色过渡箭头,并在“条件”区域中单击加号。选择动画状态,等于,并输入值 2。这个值 2 对应于我们刚刚编写的脚本中枚举的值 2。
现在,为“玩家-向西走”、“玩家-向北走”和所有“玩家-空闲”状态转换箭头逐一选择每个白色转换箭头。通过 Inspector 窗口为它们添加一个条件,并从CharStates
枚举中输入相应的值:
enum CharStates
{
walkEast = 1,
walkSouth = 2,
walkWest = 3,
walkNorth = 4,
idleSouth = 5
}
在浏览每个过渡箭头时,请记住取消选中“退出时间”、“固定持续时间”、“可以过渡到自己”等框,并将过渡持续时间(%)设置为 0。
最后一件事,我保证!选择每个玩家行走动画状态对象并将速度调整为 0.6,将每个空闲动画调整为 0.25。这将使我们的球员动画看起来刚刚好。
你现在已经设置了我们游戏所需的大部分玩家动画。按下播放按钮,用箭头键或 W,A,S,d 在屏幕上移动我们的角色。
继续伸展你像素化的腿。
小费
如果您忘记了 C# 中某个方法的确切参数,Visual Studio 将显示一个有用的弹出窗口,其中包含此信息(图 3-43 )。您可以按下 return 键来自动完成方法调用。
图 3-43
Visual Studio 显示一个带有方法参数名称和类型的弹出窗口
摘要
在这一章中,我们已经介绍了很多制作 Unity 游戏所需的核心知识。我们讲述了 Unity 工作原理背后的一些设计哲学和计算机科学原理。我们介绍了 Unity 中的游戏是如何由场景组成的,场景中的所有东西都是游戏对象。我们了解了碰撞器和刚体组件如何一起工作来确定两个游戏对象何时碰撞,以及 Unity 的物理引擎应该如何处理交互。我们了解了标签是如何在游戏运行时从脚本中引用游戏对象(如 PlayerObject)的。我们添加到工具包中的另一个有用的工具是层,它用于将游戏对象组合在一起。然后,我们可以通过脚本将逻辑强加到这些层上。
我们在本章学到的最有用的概念之一是预置,我们认为它是预制的资源模板,我们用它来创建这些资源的新副本。例如,我们的游戏可能会在游戏过程中出现数百个敌人,甚至是同时出现(如果你真的想要杀死玩家)。我们创建了一个敌人预设,并从该预设实例化了敌人游戏对象的新副本,而不是创建数百个单独的敌人游戏对象。我们已经开始学习如何编写 Unity 脚本,我们将在本书中继续学习这些知识。我们甚至编写了第一个脚本,通过移动 PlayerObject 转换组件让玩家在屏幕上走动。我们的脚本还设置了 Animator 状态机用来控制玩家状态和动画剪辑之间的转换的动画参数。我们在这一章里讲了很多,但是我们才刚刚开始!
四、世界的构建
现在我们已经学会了如何创建基本的角色动画并改变它们之间的状态,是时候为这些角色创建一个居住的世界了。二维(2D)世界通常是通过将一系列瓷砖放在一起以绘制背景,然后将其他瓷砖放在该背景之上以创建深度幻觉来创建的。这些图块实际上只是被分割或“切片”成方便的尺寸的精灵,通常使用图块调色板来放置。设计者或开发者可以构建这些 Tilemaps 的多个层来创建诸如树、头顶上飞翔的鸟、甚至远处的山之类的效果。在本章中,我们将学习如何做这些事情。你甚至可以为我们的 RPG 游戏创建自己的自定义磁贴地图。您还将学习 Unity 相机如何工作,以及如何创建行为来跟随玩家在关卡中走动。
平铺地图和平铺调色板
随着 Tilemap 功能的引入,Unity 在 2D 工作流工具链方面向前迈出了重要的一步。Unity Tilemaps 使得在 Unity 编辑器中创建关卡变得简单,而不是依赖外部工具。Unity 也有许多工具来增强 Tilemap 的特性,其中一些我们将在本章中介绍。
Tilemaps 是以特定排列存储精灵的数据结构。Unity 抽象出了底层数据结构的细节,使得开发人员可以更容易地专注于 Tilemap 的工作。
首先,我们需要导入 Tilemap 资源,就像我们在第三章中导入玩家和敌人的精灵资源一样。
在我们开始导入之前,让我们组织一下:在精灵目录中创建新的文件夹,名为:“对象”和“户外”我们将使用这些文件夹来保存 spritesheets 和 sliced sprites,用于我们的室外 Tilemap 和我们将放置在我们的世界中的各种对象。
从下载的图书资源中,在章节 4 文件夹中,找到标题为“OutdoorsGround.png”的 spritesheet。将 spritesheet 拖动到 Sprites ➤户外文件夹中。检查器中的户外导入设置应设定为以下内容:
-
纹理类型:精灵(2D 和用户界面)
-
精灵模式:多重
-
每单位像素:32
-
过滤器模式:点(无过滤器)
-
确保选择底部的默认按钮,并将压缩设置为:无
按下应用按钮。
现在,我们要切割刚刚导入的 spritesheet。在检查器中点击相应的按钮进入精灵编辑器。按左上角的切片按钮,然后从类型菜单中按像元大小选择网格。X 和 Y 像素大小使用 32 × 32。按切片按钮。
检查产生的切片线看起来不错,然后按下精灵编辑器右上角的应用按钮。我们现在有了户外瓷砖套装。
接下来,我们要创建我们的 Tilemap。在层次视图中,右键单击并选择 2D 对象➤ Tilemap 以创建 Tilemap 游戏对象。您应该会看到一个名为“Grid”的游戏对象,以及一个名为“Tilemap”的子游戏对象此网格对象用于配置其子 Tilemaps 的布局。子 Tilemap 由一个像所有游戏对象一样的转换组件、一个 Tilemap 组件和一个 Tilemap Renderer 组件组成。
这个 Tilemap 组件是我们实际“绘制”瓷砖的地方。
创建瓷砖调色板
在我们绘画之前,我们需要创建一个瓷砖调色板,它是由单独的瓷砖组成的。转到菜单窗口➤瓷砖调色板显示瓷砖调色板窗格。将“互动程序面板”面板停靠在与检查器相同的区域。
我们希望我们的项目保持有序,所以在我们的项目视图中,在主资产文件夹下创建一个名为“TilePalettes”的文件夹,然后在 Sprites 文件夹下创建另一个名为“Tiles”的文件夹。在图块文件夹中,创建两个名为“户外”和“对象”的文件夹您的项目视图应该类似于图 4-1 。
图 4-1
创建文件夹后的项目视图
在图块调板窗口中选择“创建新调板”按钮。将调色板命名为“Outdoor Tiles”,并保留网格和单元大小设置,如图 4-2 所示。
图 4-2
创建新的拼贴调色板
按“创建”并将图块调色板保存到新创建的图块调色板文件夹中。这将创建一个 TilePalette 游戏对象。
在项目视图中选择“精灵➤户外”文件夹,然后从停靠的位置选择“平铺调色板”视图。我们将使用之前导入并切片的 Outdoors spritesheet 创建一个瓷砖调色板。
选择 Outdoors spritesheet 并将其拖动到 Tile Palette 区域,在那里显示“将 Tile、Sprite 或 Sprite 纹理资源拖动到此处”
当系统提示“将图块生成到文件夹”时,导航到我们之前创建的精灵➤图块➤室外图块文件夹,然后按“选择”按钮。Unity 现在将从单独切片的精灵中生成 TilePalette。过一会儿,您应该会看到我们的户外 spritesheet 中的瓷砖出现在瓷砖面板中。
用瓷砖调色板绘画
现在有趣的部分来了:我们将使用我们的 Tile 调色板来绘制 Tilemap。
从拼贴调色板中选择画笔工具,然后从拼贴调色板中选择一个拼贴。使用画笔在场景视图中的 Tilemap 上绘制。如果写错了,可以按住 Shift 键,把瓷砖画笔当橡皮擦用。当画笔被选中时,您可以按住 Option (Mac)/Alt (PC) +鼠标左键来平移 Tilemap。
使用 Option (Mac)/Alt (PC) +鼠标左键在图块调色板周围平移,左键单击以选择图块,左键单击并拖动以选择一组图块。如果您的鼠标有滚轮,您可以使用它来放大和缩小图块调板,或者您可以按住 Option / Alt +在触摸板上上下滑动来放大和缩小。这些相同的键和手势也适用于平铺地图。
画你的磁贴地图,玩得开心!你可以让你的磁贴看起来像你喜欢的那样,但是这里有一个如何开始的建议(图 4-3 )。
图 4-3
Tilemap 的起源
现在我们已经完成了一点绘画,让我们仔细看看瓷砖调色板中的工具。
瓷砖调色板
-
选择——选择网格或特定图块的区域
-
移动选择——在选定区域内移动
-
画笔——从图块调色板中选择一个图块,然后使用画笔在图块地图上绘画
-
方框填充——使用当前选中的图块绘制一个填充区域
-
选择新笔刷——使用 Tilemap 中现有的图块作为新笔刷
-
擦除——从磁贴地图中移除一个已绘制的磁贴(快捷键:按住 Shift 键)
-
整体填充——用当前选中的图块填充一个区域
让我们回到建立我们的水平。
从你为这本书下载的资源中,将名为“OutdoorsObjects.png”的文件拖到精灵➤对象文件夹中。检查器中的导入设置应设定为以下内容:
-
纹理类型:精灵(2D 和用户界面)
-
精灵模式:多重
-
每单位像素:32
-
过滤器模式:点(无过滤器)
-
确保选择底部的默认按钮,并将压缩设置为:无
按下应用按钮。
现在点击检查器中相应的按钮进入精灵编辑器。按左上角的切片按钮,然后从类型菜单中按单元格大小选择网格。X 和 Y 像素大小使用 32 × 32。我们正在重用我们在第三章学到的精灵切片技术。
按下切片按钮,并检查产生的白色切片线看起来像他们在正确的位置分割精灵表。按下精灵编辑器右上角的应用按钮。我们现在有了一组户外主题的物体精灵来放置在我们的场景中。
现在,我们将创建一个瓷砖调色板来绘制这些对象精灵。返回到我们的图块调色板,并从下拉列表中选择“创建新调色板”。将新的调色板命名为“室外对象”,然后按“创建”按钮。出现提示时,将此调色板保存到 TilePalettes 文件夹中,我们之前在该文件夹中保存了室外瓷砖调色板。
现在,我们将做与我们为室外瓷砖所做的相同的事情:选择室外对象 spritesheet,并将其拖动到瓷砖调色板区域,在那里它说,“拖动瓷砖,精灵或精灵纹理资产在这里。”
当提示“生成瓷砖到文件夹”,导航到我们创建的精灵➤瓷砖➤对象文件夹,并按下选择按钮。Unity 现在将从单独切片的精灵中生成瓷砖调色板瓷砖。过一会儿,您应该会看到我们的对象 spritesheet 中的图块出现在图块调色板中。
小费
有时精灵是由多个瓷砖组成的。要一次选择多个单幅图块,请确保选择了画笔工具,然后单击并拖动一个矩形围绕您要使用的单幅图块。然后你就可以正常的用画笔画画了。对象 spritesheet 中的大岩石由四个独立的 sprite tiles 组成。
通过单击并拖动四块瓷砖周围的矩形,从户外对象瓷砖调色板中选择一块岩石。用画笔在你的地图上放置一块石头。您会立即注意到有些地方看起来不对:您实际上可以在岩石精灵的轮廓周围看到 Unity 场景视图的背景(图 4-4 )。
图 4-4
放置的岩石对象精灵周围的透明边框
当我们在地板砖上绘制岩石瓷砖时,我们实际上并没有在现有的瓷砖上绘制。相反,我们用新的瓷砖替换了现有的瓷砖。因为我们绘制的岩石精灵包含一些透明像素,所以我们可以看到场景视图的背景。为了避免这种情况,我们将使用多个 Tilemaps 和排序层。
使用多个切片地图
让我们把地图整理好。单击层次视图中的 Tilemap 对象,并将其重命名为:“Layer_Ground”
我们将创建多个 Tilemaps,并将它们堆叠在图层中。右键单击层次视图中的网格对象,然后转到:2D 对象➤平铺地图创建一个新的平铺地图。选择这个新的 Tilemap,并将其重命名为:“层 _ 树 _ 和 _ 岩石。”正如你可能已经从名字中猜到的,我们将在这个 Tilemap 上绘制树木、灌木丛、灌木和岩石。
在这一点上,如果你开始绘画,你会注意到又碰到了同样的透明度问题。要解决这个问题,我们必须做两件事。
要在特定的 Tilemap 上绘画,必须在 Tile Palette 视图中选择它作为活动 Tilemap 。在 Tile Palette 窗口中,你会注意到活动 Tilemap 的下拉菜单(图 4-5 )。使用它来选择我们的新层,层 _ 树 _ 和 _ 岩石。
图 4-5
选择 Layer_Trees_and Rocks 使其成为活动的 Tilemap
如果您还记得我们之前的讨论,精灵渲染器使用排序层来确定渲染精灵的顺序。在我们在 Layer_Trees_and_Rocks Tilemap 上绘制之前,我们需要为 Tilemap 设置排序层。这将确保当我们绘制树木和岩石时,它们会出现在地面瓷砖的顶部。
选择 Layer_Ground 并在检查器中找到 Tilemap 渲染器组件。
按下 Tilemap 渲染器中的添加排序层按钮,创建两个层:将第一个层称为“地面”,将第二个层称为“对象”。通过点击和拖动来重新排列这些排序层,使地面位于列表中的对象之上,如图 4-6 所示。
图 4-6
确保地面层在对象层之上
再次在“层次”视图中选择 Layer_Ground Tilemap,以在检查器中查看其属性。在 Tilemap 渲染器组件中,将排序图层更改为“Ground”选择图层 _Trees_and_Rocks Tilemap,将其排序图层更改为“Objects”
通过将活动层设置为 Layer_Ground 来删除我们之前绘制的岩石瓷砖,然后从瓷砖调色板工具集中选择擦除工具。您也可以通过按住 shift 键并绘画来使用画笔删除项目。用一些草或者任何你喜欢的室外物品面板上的地板砖填充被擦掉的地方。
现在我们准备开始绘画了。当您要绘制地面瓷砖时,请确保活动瓷砖贴图设置为 Layer_Ground,当您要绘制树木、岩石和灌木时,请确保活动瓷砖贴图为 Layer_Trees_and_Rocks。
小费
使用方括号键“[”和“]”旋转选定的拼贴,然后再使用它进行绘制。您也可以通过这种方式直接在调色板上旋转图块。
然后将激活的 Tilemap 设置为 Layer_Trees_and_Rocks,并使用 Outdoor Objects Tile 调色板绘制一些岩石和灌木丛(图 4-7 )。
图 4-7
在层 _ 树 _ 岩石磁贴上画一些岩石和灌木
现在我们的地图开始看起来像地图了。在我们的玩家开始探索之前,我们还需要做一些事情。
我们要确保玩家呈现在地面和岩石前。我们将通过设置玩家的排序层来实现这一点。选择 PlayerObject,然后在 Sprite 渲染器组件中查找排序层属性,并按“添加排序层”按钮。添加一个名为“字符”的排序层,并将其移动到底部,在地面和物体层之后。现在我们已经告诉 Sprite 渲染器按照从第一个排序层“地面”到最后一个排序层“角色”的顺序渲染对象
你的排序层应该如图 4-8 所示。
图 4-8
添加字符排序层
选择 PlayerObject 并将其排序层设置为我们刚刚创建的 Characters 层。这将使玩家站在地面和地面上的任何物体上,并使角色看起来像是在地面上行走。
我们将在本章后面解释相机是如何工作的,但是现在,选择相机对象并将大小属性更改为 3.75。
按下播放按钮,带我们的玩家在小岛上散步。
您会立即注意到一些事情:
-
镜头不跟着球员。事实上,如果你想的话,你可以直接走出屏幕,一直走下去。
-
玩家可以穿过地图上的物体。
-
您可能会在 Tilemap 上看到一些奇怪的线条或“眼泪”。如果它们出现,它们将位于两块瓷砖之间。
我们将在本章中讨论所有这些问题。
我们将学习使用碰撞器来防止玩家走过所有东西,我们将使用一个叫做 Cinemachine 的工具来让摄像机跟随玩家行走。我们还要确保摄像机配置正确。我们将配置图形设置,以确保我们得到一个清晰的边缘,这对像素艺术很重要,我们将使用一种材料来消除眼泪。
小费
如果您有多个 Tilemap 层,但希望只关注一个层,请使用场景视图右下角的 Tilemap 聚焦模式。这将允许您灰化其他 Tilemap 图层,并专注于特定图层的工作。
图形设置
让我们调整 Unity 引擎的图形设置,使我们的像素艺术看起来尽可能好。当当前设备的图形输出不足以将对象的边缘渲染成完美平滑的线条时,Unity 使用一种称为抗锯齿的算法。对象的边缘不是呈现平滑的线条,而是呈现锯齿状或锯齿。抗锯齿算法在对象的边缘上运行,并使其具有平滑的外观,以补偿锯齿状的图形输出。
默认情况下,在 Unity 编辑器中抗锯齿是打开的,与您使用的设备的电源无关。要关闭抗锯齿,请转到编辑菜单➤项目设置➤质量,并将抗锯齿设置为禁用。正如我们所知,Unity 引擎可以用于 3D 和 2D 游戏,但我们不需要为我们的像素艺术风格的 2D 游戏抗锯齿。
从同一个菜单中,编辑➤项目设置➤质量,也禁用各向异性纹理。当使用特定类型的相机透视时,各向异性过滤是增强图像质量的一种方式。它与我们正在做的项目无关,所以我们应该关闭它。
照相机
Unity 中的所有 2D 项目都使用一种叫做正交摄像机的东西。正交相机渲染大小相同的远近物体。通过将所有对象渲染成相同的大小,在旁观者看来,好像所有东西离摄像机的距离都是相同的。这与 3D 项目渲染对象的方式不同。在 3D 项目中,对象以不同的大小渲染,以产生距离和透视的错觉。当我们建立一个 2D 项目时,我们在一开始就配置了我们的 Unity 项目使用正交摄像机。
为了在渲染 2D 图形时获得最佳效果,理解相机在 2D 游戏中的工作方式很重要。正交摄像机有一个称为大小的属性,它决定了屏幕高度的一半可以容纳多少个垂直的世界单位。世界单位通过在 Unity 中设置 PPU 或每单位像素来确定。正如你可能从名字中怀疑的那样,每单位像素设置描述了 Unity 引擎应该在一个世界单位中渲染多少像素,即每单位像素*。PPU 可以在导入资产过程中设置。PPU 是很重要的,因为当你为你的游戏创作时,你会希望确保所有的东西在同一个 PPU 下看起来都很好。*
摄像机尺寸的计算公式是:
(垂直分辨率/ PPU) * 0.5 =摄像头尺寸
让我们用几个简单的例子来阐明这个概念。
给定屏幕分辨率为 960 × 640,垂直屏幕高度为 640 像素。让我们使用 64 的 PPU,使我们的计算简单:640 除以 64 等于 10。这意味着 10 个世界单位相互堆叠将占据整个垂直屏幕高度,5 个世界单位将占据垂直屏幕高度的一半。因此,摄像机尺寸为 5,如图 4-9 所示。
图 4-9
分辨率为 960 × 640,PPU 为 64,因此相机大小为 5
再来做一个例子。如果你的游戏使用 1280 × 1024 的屏幕分辨率,那么垂直屏幕高度就是 1024。使用 32 的 PPU,我们将 1024 除以 32 得到 32。这意味着 32 个世界单位相互堆叠将占据整个垂直屏幕高度,16 个世界单位将占据垂直屏幕高度的一半。因此,正交相机大小为 16。
这里还有最后一个例子来加强这个等式。使用 1280 × 720 的屏幕分辨率,垂直屏幕高度将为 720。使用 32 的 PPU,我们将 720 除以 32 得到 22.5。这意味着 22.5 个世界单位堆叠在一起将适合垂直屏幕高度:22.5 除以 2 等于 11.25,这是屏幕高度和我们的正交相机尺寸的一半。
开始掌握诀窍了吗?正投影尺寸乍一看似乎很奇怪,但实际上,这是一个非常简单的等式。
这又是一个等式:
(垂直分辨率/ PPU) * 0.5 =摄像头尺寸
获得好看的像素艺术游戏的诀窍是注意与分辨率相关的正交相机尺寸,并确保艺术作品在某个 PPU 下看起来不错。
在我们的游戏中,我们将使用 1280 × 720 的分辨率,但我们将使用一个技巧来放大图片。我们将 PPU 乘以比例因子 3。
我们修改后的等式将如下所示:
(垂直分辨率/ (PPU 缩放因子)) 0.5 =摄像机尺寸
使用 1280 × 720 的分辨率和 32 的 PPU:
(720 / (32 PPU * 3)) * 0.5 = 3.75 摄像头尺寸
这就是为什么我们把相机尺寸提前到 3.75 的原因。
现在我们已经更好地了解了相机在正交游戏中的工作原理,让我们来设置屏幕分辨率。Unity 自带多种屏幕分辨率选择,但有时设置自己的分辨率也是有益的。我们将设置 1280 × 720 的分辨率,这被认为是“标准高清”,应该足以满足我们正在制作的游戏风格。
点击游戏窗口,寻找屏幕分辨率下拉菜单。默认情况下,可能会设置为自由方面,如图 4-10 所示。
图 4-10
下拉菜单
在下拉菜单的底部,按加号打开一个窗口,您可以在其中输入新的分辨率。创建一个 1280 × 720 的自定义分辨率,如图 4-11 所示。
图 4-11
创建新的自定义分辨率
按下播放按钮,让角色在地图上走来走去,看看我们新的分辨率和相机的效果。
令人兴奋的东西!我们的游戏开始变得像…嗯,一个游戏!
我们已经为玩家创建了一个地图,但是你可能已经注意到了,摄像机停留在一个地方。这对于某些类型的游戏来说很好,比如益智游戏,但是对于 RPG 来说,我们需要摄像机跟随玩家。可以编写一个 C# 脚本来引导相机跟随玩家,但我们将使用一个名为 Cinemachine 的 Unity 工具来代替。
注意
Cinemachine 最初由亚当·迈希尔创作,在 Unity 资产商店出售。Unity 最终收购了 Cinemachine,并将其作为免费服务的一部分。如第二章所述,您可以创建自己的工具、作品和内容,并在 Unity Asset Store 中出售。
使用电影胶片
Cinemachine 是一套功能强大的 Unity 工具,用于程序游戏中的摄像机、电影和过场动画。Cinemachine 可以自动执行所有类型的摄像机移动,自动在摄像机之间混合和剪切,并自动执行所有类型的复杂行为,其中许多行为超出了本书的范围。我们将使用 Cinemachine 自动跟踪玩家在地图上的行走。
Cinemachine 通过 Unity 2017.1 的资产商店提供,但从 Unity 2018.1 开始,Cinemachine 通过新的 Unity Package Manager 提供。Unity 的早期版本仍然可以使用资产商店中的 Cinemachine,但该版本不再更新,也不会包含新功能。
稍后我们将讨论如何在 Unity 2017 和 Unity 2018 中安装 Cinemachine。请参阅您正在运行的 Unity 版本的说明。
在 Unity 2017 中安装 Cinemachine
转到“窗口”菜单,选择“资产存储”以打开“资产存储”选项卡。在屏幕顶部的搜索栏中,键入“Cinemachine”,然后按 enter 键。您应该会得到类似图 4-12 的结果。
图 4-12
资产商店中的 Cinemachine Unity 套装
点击 Cinemachine 图标,进入资产页面。在“资产”页面上,按“导入”按钮将 Cinemachine Unity 软件包导入到您当前的项目中。Unity 将向您呈现一个弹出屏幕,如图 4-13 所示,显示包内的所有资产。按导入按钮。
图 4-13
导入 Cinemachine Unity 软件包
导入 Cinemachine 包应该已经创建了一个名为“Cinemachine”的新文件夹。
在 Unity 2018 中安装 Cinemachine
从菜单中,选择窗口➤软件包管理器。您应该会看到 Unity 软件包管理器窗口出现。选择 All 选项卡,如图 4-14 所示,然后选择 Cinemachine。
图 4-14
选择全部选项卡
点击右上角的安装按钮安装 Cinemachine。Cinemachine 安装完成后,关闭软件包管理器窗口。您应该在项目视图中看到一个新的 Packages 文件夹。
安装 Cinemachine 后
不管你运行的是哪个版本的 Unity,当 Cinemachine 安装完成后,你应该会在屏幕顶部看到一个 Cinemachine 菜单,在组件和窗口之间。
注意
Unity 包是可以放入项目中的文件集合,开箱即用。软件包以模块化、版本化的形式出现,并自动解析依赖关系。2018 年 5 月,Unity 宣布包是未来,他们打算通过包分发他们的许多新功能。
虚拟摄像机
进入 Cinemachine 菜单,选择创建 2D 相机。这应该创建两个对象:一个 Cinemachine Brain ,连接到主摄像头,和一个 Cinemachine 虚拟摄像头游戏对象,名为“CM vcam1”。
什么是虚拟摄像头?Cinemachine 文档使用了一个很好的类比——虚拟摄像机可以被认为是摄影师。该摄影师控制主摄像机的位置和镜头设置,但实际上不是摄像机。虚拟相机可以被认为是一个轻量级的控制器,它指导主相机并告诉它如何移动。我们可以为虚拟相机设置一个跟随的目标,沿着一条路径移动虚拟相机,从一条路径混合到另一条路径,并围绕这些行为调整所有类型的参数。虚拟摄像机是 Unity 游戏开发工具箱中一个非常强大的工具。
Cinemachine Brain 是场景中主摄像机和虚拟摄像机之间的实际链接。Cinemachine 大脑监控当前活动的虚拟摄像机,然后将其状态应用到主摄像机。在运行时打开和关闭虚拟相机,Cinemachine 大脑可以将相机混合在一起,获得一些非常惊人的结果。
选择虚拟摄像机并将 PlayerObject 拖动到名为“Follow”的属性中,如图 4-15 所示。
图 4-15
将虚拟摄影机跟随目标设置为 PlayerObject
这告诉 Cinemachine 虚拟摄像机在玩家在地图上移动时跟随并跟踪玩家游戏对象的变换组件。
按下播放键,看着摄像机跟着玩家转。相当整洁!有了 Cinemachine,我们只需点击几下鼠标,就可以获得一些非常复杂的相机行为。为了更好地理解控制相机运动的隐藏参数,让我们隐藏地面层。从项目视图中选择 Layer_Ground Tilemap 对象。取消选中 Tilemap 的 Tilemap 渲染器组件旁边的复选框以将其禁用。现在 Unity 不会渲染 Layer_Ground Tilemap 了。你的场景应该类似于图 4-16 ,隐藏所有的地面瓷砖。
图 4-16
在取消选中“Tilemap Renderer”左侧的复选框以禁用它之后
现在点击层次视图中的主相机对象,并按下标有背景的彩色框。将背景颜色改为白色(图 4-17 )。这将使得在下一步中更容易看到 Cinemachine follow 帧。
图 4-17
将相机背景颜色改为白色
最后,选择虚拟相机,并确保“游戏窗口指南”复选框被选中。下一步你会看到什么是游戏窗口指南。
再次按下播放按钮。请注意,中间有一个白色的盒子围绕着玩家,周围是浅蓝色的区域,还有一个红色的区域包围着它(图 4-18 )。这个白色的盒子被称为“死区”死区内有一个黄色点,称为跟踪点,它将直接随着玩家移动。
图 4-18
玩家周围的死区包含一个黄色跟踪点
玩家周围的死区是跟踪点可以移动的区域,摄像头不会移动跟随。当跟踪点移出死区并进入蓝色区域时,摄像机将移动并开始跟踪。Cinemachine 也会给机芯增加一点阻尼。如果你能以某种方式足够快地移动,让球员进入红色区域,摄像机将 1:1 地跟踪球员,没有延迟地跟踪每个动作。
确保游戏视图是可见的,并点击白色框的边缘。将白色框向外拖一点,以调整跟踪区域的大小,使其变大一点。现在,玩家可以在不移动相机的情况下走得更远一点。您可以调整这些引导线的大小,以在游戏中获得自然的相机行为。
在层次视图中 Cinemachine 对象仍处于选中状态的情况下,查看 Cinemachine 虚拟摄影机组件。您将看到一个展开“Body”部分的箭头。在机身部分内,(图 4-19 )有调整虚拟相机机身 X 和 Y 阻尼的选项。阻尼是虚拟摄像机死区移动以赶上跟踪点的速度。
图 4-19
虚拟相机机身属性部分的阻尼属性
理解阻尼的最好方法是当你带玩家在地图上走动时调整 X 和 Y 阻尼。按 Play 并试验阻尼值。
如果你把玩家带到地图的边缘,你会看到相机随着目标移动,事情看起来不会太糟。但是我们可以做得更好。
停止播放并在层次视图中选择 Layer_Ground 对象。选中“Tilemap Renderer”左侧的框,使图层再次可见。
电影展
现在我们知道了如何让摄像机跟踪玩家走动,我们将学习当玩家接近屏幕边缘时如何防止摄像机移动。我们将使用一个叫做 Cinemachine Confiner 的组件来将摄像机限制在某个区域。Cinemachine Confiner 将使用一个碰撞器 2D 对象,我们已经预先配置了它来包围我们想要约束摄像机的区域。
在我们进入实现细节之前,让我们想象一下 Confiner 将如何影响摄像机的移动。请记住,虚拟摄影机实际上是在指挥活动场景摄影机,告诉它向哪里移动以及以什么速度移动。
在图 4-20 中,我们有一个场景中的玩家,他正准备向东走。
图 4-20。玩家正准备向东走
白色区域是当前活动摄影机的可见视口。灰色区域是贴图的其余部分,在摄影机的视口之外,当前不可见。灰色区域的外围被一个 2D 对撞机所包围。
当玩家向东行走时,虚拟相机会引导相机向东移动,并在玩家走过场景时跟踪玩家,如图 4-21 所示。
图 4-21。玩家正在向东走
虚拟相机的移动将考虑玩家的移动速度、死区的大小以及应用于相机机身的阻尼量。
要记住的关键是,我们已经用多边形碰撞器 2D 用灰色圈住了这个区域的周界,并设置了包围器的边界形状指向这个碰撞器。当约束器边缘碰到边界形状的边缘时,它会相互作用并告诉虚拟摄像机引导活动摄像机停止移动,如图 4-22 所示。
图 4-22。碰撞器撞到了 2D 多边形碰撞器的边缘,相机停止了移动
正如你在前面的图中看到的,Confiner 边碰到了包围着关卡的边界图形的边,这个边界图形就是碰撞器 2D。虚拟相机已经停止移动,玩家继续走到地图边缘。
让我们建造一个电影院。
从层级视图中选择我们的虚拟摄像机。在检查器中,在添加扩展旁边,从下拉菜单中选择 CinemachineConfiner。这将为我们的 Cinemachine 2D 摄像机增加一个 Cinemachine Confiner 组件。
CinemachineConfiner 需要一个复合对撞机 2D,或者一个多边形对撞机 2D 来确定限制的边缘从哪里开始。选择 Layer_Ground 对象,并通过“添加组件”按钮添加多边形碰撞器 2D。点击碰撞器组件上的编辑碰撞器按钮,编辑碰撞器,使其包围我们的 Layer_Ground 层的边缘,如图 4-23 所示。
图 4-23
拖动多边形碰撞器 2D 的角以匹配 Layer_Ground 的轮廓
图 4-23 中的箭头提醒你在碰撞器和地图边缘之间留一点空间。这是为了让相机显示一点水,而不是严格限制在陆地的边缘。当你完成编辑碰撞器时,不要忘记再次按下编辑碰撞器按钮。检查碰撞器组件的“Is Trigger”属性,然后再次选择我们的 Cinemachine 摄像机。我们想使用这个碰撞器作为触发器,因为如果我们不这样做,当玩家的碰撞器和 Tilemap 碰撞器交互时,玩家将被强行推出碰撞器。这是因为两个带有碰撞器的物体不能占据同一个地方,除非其中一个被用作触发器。
选择并拖动 Layer_Ground 对象到 Cinemachine Confiner 的边界形状 2D 区域,如图 4-24 所示。
图 4-24
来自 Layer_Ground 的多边形碰撞器 2D 将用于 2D 的边界形状
Confiner 将从 Layer_Ground 对象中获取碰撞器 2D,并将其用作 Confiner 的边界形状。确保选中“限制屏幕边缘”框,告诉限制程序停止在多边形 2D 边缘。
按下播放按钮,走到屏幕边缘。如果一切都设置正确,你会看到虚拟相机的死区停止移动,只要相机到达我们之前放置多边形碰撞器 2D 的边缘。图 4-25 中的箭头指向多边形碰撞器 2D 的边缘。正如你所看到的,玩家已经走出死区很远,当跟踪点继续随着玩家移动时,摄像机已经停止。
图 4-25
死区已经停止随着玩家移动
回顾一下,设置 Cinemachine Confiner 的三个步骤:
-
向虚拟摄像机添加 CinemachineConfiner 扩展。
-
在 Tilemap 上创建一个多边形碰撞器 2D,编辑其形状以确定限制边,并设置“是触发器”属性。
-
使用这个多边形碰撞器 2D 作为 Cinemachine Confiner 的边界形状 2D 场。
迫使相机在屏幕边缘停止移动,同时允许玩家继续行走,这是你可能在几十个 2D 游戏中见过的常见效果。
请注意,使用 Confiner 不会阻止玩家离开地图——只是相机会跟踪他们。我们将很快设置一些逻辑来防止玩家离开地图。
稳定化
当你带玩家在地图上走动时,你可能会注意到轻微的抖动效果。当你停止行走时,抖动尤其明显,虚拟相机阻尼慢慢使跟踪停止。这种抖动效果是由于过于精确的相机坐标。摄像机正在跟踪玩家,但是它移动到子像素位置,而玩家只是在像素间移动。我们之前在计算正交相机尺寸时已经确定了这一点。
为了解决这种抖动,我们希望强制 Cinemachine 虚拟相机的最终位置保持在像素边界内。我们将编写一个简单的“扩展”组件,并将其添加到 Cinemachine 虚拟摄像机中。我们的扩展组件将抓取 Cinemachine 虚拟相机的最后一个坐标,并将它们四舍五入到与我们的 PPU 一致的值。
创建一个名为 RoundCameraPos 的新 C# 脚本,并在 Visual Studio 中打开它。键入以下脚本,并参考以下注释以更好地理解它。这当然是你将要编写的更高级的脚本之一,但是如果让你的游戏看起来漂亮对你来说很重要,理解它是值得的。
using UnityEngine;
// 1
using Cinemachine;
// 2
public class RoundCameraPos : CinemachineExtension
{
// 3
public float PixelsPerUnit = 32;
// 4
protected override void PostPipelineStageCallback(
CinemachineVirtualCameraBase vcam,
CinemachineCore.Stage stage, ref CameraState state, float deltaTime)
{
// 5
if (stage == CinemachineCore.Stage.Body)
{
// 6
Vector3 pos = state.FinalPosition;
// 7
Vector3 pos2 = new Vector3(Round(pos.x), Round(pos.y), pos.z);
// 8
state.PositionCorrection += pos2 - pos;
}
}
// 9
float Round(float x)
{
return Mathf.Round(x * PixelsPerUnit) / PixelsPerUnit;
}
}
对之前代码的解释是:
// 1
using Cinemachine;
导入 Cinemachine 框架来编写一个扩展组件,我们将把它附加到 Cinemachine 虚拟摄像机上。
// 2
public class RoundCameraPos : CinemachineExtension
挂钩到 Cinemachine 的处理管道的组件必须从 CinemachineExtension 继承
// 3
public float PixelsPerUnit = 32;
每单位像素,或 PPU。正如我们之前讨论相机时所讨论的,我们在一个世界单位中显示 32 个像素。
// 4
protected override void PostPipelineStageCallback(CinemachineVirtualCameraBase vcam, CinemachineCore.Stage stage, ref CameraState state, float deltaTime)
从 CinemachineExtension 继承的所有类都需要此方法。在 Confiner 完成处理后,Cinemachine 会调用它。
// 5
if (stage == CinemachineCore.Stage.Body)
Cinemachine 虚拟摄像机有一个由几个阶段组成的后处理管道。我们执行此检查,以查看我们处于相机后处理的哪个阶段。如果我们在“身体”阶段,那么我们被允许设置虚拟相机在空间中的位置。
// 6
Vector3 finalPos = state.FinalPosition;
检索虚拟摄像机的最终位置
// 7
Vector3 newPos = new Vector3(Round(finalPos.x), Round(finalPos.y), finalPos.z);
调用我们写的 Rounding 方法(如下)对位置进行舍入,然后用结果创建一个新的 Vector。这将是我们新的、像素限定的位置。
// 8
state.PositionCorrection += newPos - finalPos;
将 VC 的新位置设置为旧位置和我们刚刚计算的新舍入位置之间的差值。
// 9
对输入值进行舍入的方法。我们使用这种方法来确保相机总是停留在一个像素位置。
材料
当你带着玩家在地图上走的时候,你可能会注意到方块之间的一些线或“裂缝”。这是因为它们没有精确地捕捉到像素级的位置。为了解决这个问题,我们将使用一种叫做材质的东西来告诉 Unity 我们想要如何渲染我们的精灵。
创建一个名为“材料”的新文件夹,然后右键单击并创建➤材料。称这种材料为“Sprite2D”
按如下方式设置材料的属性:
-
着色器:sprite/default
-
确保选中像素捕捉。
新的材料属性应该如图 4-26 所示。
图 4-26
配置新材料
我们希望游戏对象中的渲染器组件使用这种材质,而不是默认材质。
选择我们的 Layer_Ground Tilemap,并通过单击 material 属性旁边的点来更改 Tilemap 渲染器中的材质。当你选择了 Sprite2D 材质后,渲染器组件看起来应该如图 4-27 所示。
图 4-27
在我们的 Tilemap 渲染器组件中使用 Sprite2D 材质
对我们所有的 Tilemap 图层都这样做,然后按下播放按钮,眼泪应该已经消失了。
碰撞器和 Tilemaps
Tilemap 对撞机
现在我们要解决的问题是,玩家可以在 Tilemap 上浏览任何东西。还记得我们在第三章中如何给我们的 player 对象添加一个 2D 碰撞器吗?有一个专门为 Tilemap 定制的组件,叫做 Tilemap Collider 2D。当一个 Tilemap 碰撞器 2D 被添加到一个 Tilemap 时,Unity 将自动检测并添加一个碰撞器 2D 到它在那个 Tilemap 上检测到的每个 sprite tile。我们将使用这些 Tilemap 碰撞器来确定 PlayerObject 碰撞器何时与 tile 碰撞器接触,并防止玩家走过它。
从层次视图中选择 Layer_Trees_and_Rocks,然后按检查器中的“添加组件”按钮。搜索并添加一个名为“Tilemap Collider 2D”的组件。
你会注意到 Layers_Objects Tilemap 上的所有精灵现在都有一条绿色细线围绕着它们,表示一个碰撞器组件,类似于图 4-28 。
图 4-28
如箭头所示,2D 在岩石上添加了对撞机
注意
如果您在 Tilemap 上看到每个图块周围都有一个框,则您选择了错误的 Tilemap (Layer_Ground)。这是一个常见的错误。点击检查器中组件右上角的齿轮图标,移除 Tilemap Collider 2D 组件,然后从菜单中选择移除组件,如图 4-29 所示。
图 4-29
移除放错位置的 Tilemap 碰撞器 2D 组件
现在在层次视图中选择想要的 Tilemap: Layer_Trees_and_Rocks,并添加一个 Tilemap 碰撞器 2D 组件到其中。
我们刚刚给 Layer_Objects 上的每个贴图精灵添加了一个 2D 碰撞器。看一下图 4-30 ,注意花园周围的灌木丛是如何拥有七个独立的碰撞器的。问题是 Unity 跟踪所有这些碰撞器的效率很低。
图 4-30
层 _ 树 _ 岩石中的每个精灵现在都有自己的碰撞器
复合对撞机
幸运的是,Unity 附带了一个叫做复合对撞机的工具,它将所有这些独立的对撞机组合成一个大型对撞机,效率更高。在层次视图中保持 Layer_Trees_and_Rocks Tilemap 层处于选中状态,选择“添加组件”并将复合碰撞器 2D 组件添加到 Layer_Trees_and_Rocks。您可以保留所有默认设置。现在检查 2D tile map 碰撞器上的方框,上面写着“由复合材料使用”,看看所有用于布什的独立碰撞器是如何像变魔术一样合并在一起的。
当我们在 Tilemap 层添加一个复合碰撞器 2D 时,Unity 自动添加了一个刚体 2D 组件。设置这个刚体 2D 组件身体类型为静态,因为它不会移动。
在我们按下 Play 之前,我们先确定一下当玩家撞到什么东西的时候,她没有转过来,如图 4-31 所示。因为 PlayerObject 有一个动态刚体 2D 组件,所以当它与其他碰撞器交互时,会受到物理引擎施加的力的影响。
图 4-31
这个看起来荒谬的旋转是由于刚体 2D 碰撞造成的
选择 PlayerObject,在附加的刚体 2D 组件中,勾选“冻结旋转 Z”复选框,如图 4-32 所示。
图 4-32
冻结 Z 轴旋转以防止玩家旋转
按下播放按钮,让玩家在地图上走一圈。你会注意到她再也不能穿过灌木丛,岩石,或者任何你放在树木和岩石层的东西。这是因为我们在第三章中添加到 PlayerObject 的碰撞器与我们刚才添加的 Tilemap 碰撞器发生了碰撞。
您还会注意到,对于某些对象,播放器停止的位置和 Tilemap 上的对象之间存在明显的差距。为了更好地查看每个碰撞器的边界,保持游戏运行并通过选择场景选项卡切换到场景视图。
使用鼠标或触摸板上的滚轮放大播放器。如果需要,可以通过按 Alt (PC)或 Option (Mac)键,然后单击并拖动 Tilemap 来平移场景。从层次视图中选择 PlayerObject 以查看其长方体碰撞器。然后按住 Control (PC)或 Cmd / ⌘ (Mac)并选择 Layer_Trees_and_Rocks TileMap,而不取消选择 PlayerObject。
现在两个游戏对象都应该被选中了,你应该在玩家周围看到一个碰撞器,在 Tilemap 中的瓷砖周围看到另一个碰撞器。取决于你如何绘制你的 Tilemap,确切的瓷砖会有所不同,但是正如你在图 4-33 中看到的,碰撞器框为每个物体显示为细绿线。
图 4-33
玩家和她周围的物体之间的距离是由碰撞盒造成的——薄薄的绿色盒子
石头和玩家的碰撞器发生碰撞,阻止玩家靠近。因为碰撞器没有紧紧地拥抱岩石,在玩家停止的地方和岩石之间有一个明显的间隙。我们可以通过编辑每种精灵的物理形状来解决这个问题。
编辑物理形状
要编辑 spritesheet 中的 Sprite 的物理形状,请在项目视图中选择 Outdoor Objects spritesheet,并在检查器中打开 Sprite 编辑器。进入左上角的精灵编辑器下拉菜单,选择编辑物理形状,如图 4-34 所示。
图 4-34
在精灵编辑器中选择“编辑物理”形状
选择要编辑的精灵,并按下“轮廓公差”旁边的更新按钮,以查看精灵周围的物理形状轮廓。
拖动方框以匹配您想要的对象轮廓(图 4-35 )。除非你的游戏机制真的依赖它,否则没有必要对物理形态要求超精确。您可以通过单击线本身来创建其他点,通过选择一个点并按 Control (PC)或 Cmd / ⌘ (Mac) + delete 来删除点。
图 4-35
将物理形状与精灵相匹配
当你对物理图形感到满意时,按下应用按钮并关闭精灵编辑器。要在场景中使用这个新的物理轮廓,请确保选择了相关的 Tilemap,并从 Tilemap 碰撞器 2D 组件上的齿轮图标下拉菜单中按下重置按钮,如图 4-36 所示。这将迫使 Unity 编辑器读取更新的物理形状信息。
图 4-36
重置 Tilemap 碰撞器 2D 组件以使用新的物理图形
现在按下播放按钮,看看你的新的和改进的碰撞器是如何工作的。
小费
Unity 在制作复合碰撞器时,在合并碰撞器方面做出了最好的猜测,因此如果您在 Tile Editor 中调整精灵的物理轮廓时在精灵周围留下了间隙,您可能不会看到所有的 Tile 合并到一个巨大的碰撞器中。您可以再次在平铺编辑器中调整物理轮廓,或者如果没有太多间隙,则保留它。记住:如果你调整物体的物理轮廓,你需要每次重置组件来获得更新的物理轮廓。
因为你现在是碰撞器方面的专家,你可能还想把我们播放器上的箱式碰撞器 2D 调小一点,如图 4-37 所示。
图 4-37
调整我们播放器上的碰撞器大小,使之更适合
现在我们已经熟悉了 Tilemap 碰撞器,让我们用它们在地图上创建一个围绕陆地的边界,这样玩家就不能走进水里了。你的游戏可能有不同的要求——你可能出于某种原因希望玩家走进水里。但接下来是几种不同的技术之一,以防止球员走进你不想让他们进入的区域。
选择您的 Layer_Ground,并从您不想让玩家进入的区域移除任何牌。在我们创建的示例地图中,我们将移除水瓷砖,因为我们不希望玩家走进水中。我们要移除这些瓷砖,因为我们要将它们绘制到不同的图层上。现在创建一个新的 Tilemap 层,名为“层 _ 水”。确保将新图层上的排序图层设置为地面,以保持一致。
确保选择新创建的层作为平铺调色板屏幕中的活动平铺地图。画出你想让玩家远离的区域,比如水,如图 4-38 所示。请注意,在图 4-38 中,我们将焦点设置为 Tilemap,因此我们只能看到当前所选 Tilemap 层的图块。
图 4-38
打开焦点可以更清楚地看到新的 Tilemap 图层
我们想添加一个 Tilemap 碰撞器 2D 和一个复合碰撞器 2D 到层 _ 水 Tilemap。添加复合碰撞器 2D 将自动添加一个刚体 2D 组件。将刚体 2D 身体类型设置为静态,因为我们不希望海洋瓷砖在与玩家碰撞时移动。最后,在图块碰撞器 2D 中选中“用于合成”框,将所有单独的图块碰撞器合并成一个高效的碰撞器。
按下播放按钮,注意玩家是如何无法再走进水中的。我们在这里用对撞机所做的并不是什么新鲜事。在本章的前面,当我们使用 Tilemap 碰撞器时,你已经做了这种事情。
摘要
在这一章中,我们已经介绍了用 Unity 制作 2D 游戏的一些核心概念。我们学习了如何将精灵变成瓷砖调色板,并用它们来绘制瓷砖地图。我们使用碰撞器来防止玩家穿过物体,以及如何调整它们来获得更好的玩家体验。我们学习了如何配置相机,以实现缩放比例、艺术尺寸和分辨率之间的平衡,这在 2D 像素艺术风格的游戏中非常重要。我们在这一章中提到的最有价值的工具之一是 cinema Chine——一个自动化摄像机运动的强大工具。如果你有兴趣了解更多关于 Cinemachine 的信息, https://forum.unity.com
是一个提问和向它的创造者学习的好地方!在第五章中,你将会看到我们到目前为止所学的东西汇集在一起,你将会开始觉得你真的在做一个游戏。