Unity3D-游戏开发指南-全-
Unity3D 游戏开发指南(全)
原文:
zh.annas-archive.org/md5/9b293d9bf8d78d403c01057bf2543097译者:飞龙
前言
这本书详细介绍了使用 Unity 制作的 3D 环境解谜游戏的设计和开发过程。我们探讨了设计、创建和实现角色、环境、UI、声音和游戏机制。
这本书是为谁而写的?
这本书特别适合对制作 3D 游戏感兴趣但尚未开始旅程的人。我们涵盖了从基础到一些高级主题的所有内容。
其次,这本书可以帮助任何已经开始他们的旅程但想学习游戏开发其他部分的人,因为我们将在整本书中涵盖广泛的技术和知识。
本书涵盖的内容
第一部分 – 规划和设计
第一章,三维基础入门,通过 3D 术语和本书将涉及的基础术语进行了一次旅行。
第二章,设计和原型,引导用户进入设计迷宫,并以安装 Unity 来创建第一个项目结束。
第三章,编程,奠定了编程的基础。这一章节侧重于 C#(C Sharp)的力量,通过解释逻辑的基础和 Visual Studio 的初始使用来阐述。
第二部分 – 构建
第四章,角色,讨论了设计 3D 角色时需要考虑的内容,以及它们将如何用于绑定和动画。
第五章,环境,引导你思考游戏的环境以及我们是如何设计和构建环境的。
第六章,交互和机制,花时间分析机制需要如何思考以及交互对用户的意义,同时涵盖我们项目中交互所需的编程。
第七章,刚体和物理交互,增加了与物理和更高级编程概念的交互复杂性。
第八章,用户界面和菜单,介绍了 Unity 的画布组件以及如何在任何项目中开发整体游戏界面。
第三部分 – 精炼和优化
第九章,视觉效果,深入探讨了如何与视觉效果系统合作,以在你的世界中增加更深的情感联系。这是通过解释渲染的基础和围绕它的系统来实现的。
第十章,声音效果,以解释 Unity 中的声音系统以及建立坚实的声音设计基础开始。
第十一章,构建和测试,教你 Unity 如何构建最终的可执行游戏,并解释了测试方法,以根除你可以消除的 bug,从而制作出更好的产品。
第十二章,收尾工作,看起来像是一个工具箱,用于使你的游戏尽可能完美。我们回顾了我们用来完善项目的内容。这包括特定的粒子系统、照明、艺术定义和高级声音处理。
附加章节,其他 Unity 工具,是介绍 Unity 提供的一些服务的一章,以防这本书激发你去做我们尚未涵盖的项目,例如多人游戏或混合现实需求。
要充分利用本书
-
不要将本书视为教程,而应将其视为开发 3D 游戏时使用的许多工具。我们只介绍了一些简单的示例。尽可能地将逻辑应用到你的项目中。
-
准备好就所涉及的主题做自己的笔记。在物理部分,我们将编程难度提升了很多。
-
在 Discord 上提问,Discord 通过二维码与本书相连。
下载示例代码文件
该书的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Unity-3D-Game-Development。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781801076142_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”
代码块设置如下:
void OnStartGameButtonPressed()
{
SetPlayerEnabled(true);
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
this.gameObject.SetActive(false);
}
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词,也以这种方式出现在文本中。例如:“从管理面板中选择系统信息。”
警告或重要注意事项看起来像这样。
小贴士和技巧看起来像这样。
联系我们
读者的反馈总是受欢迎的。
一般反馈:请通过电子邮件feedback@packtpub.com发送反馈,并在邮件主题中提及本书的标题。如果您对本书的任何方面有疑问,请通过电子邮件questions@packtpub.com联系我们。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您向我们报告。请访问www.packtpub.com/submit-errata,点击提交勘误,并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,我们将非常感激您提供位置地址或网站名称。请通过电子邮件copyright@packtpub.com与我们联系,并提供材料的链接。
如果你有兴趣成为作者:如果你在某个领域有专业知识,并且有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享你的想法
一旦你阅读了《Unity 3D 游戏开发》,我们很乐意听听你的想法!扫描下面的二维码直接进入此书的亚马逊评论页面并分享你的反馈。

你的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
目录
-
贡献者
-
关于作者
-
关于审稿人
-
-
前言
-
这本书适合谁?
-
本书涵盖的内容
-
第一部分 – 规划和设计
-
第二部分 – 构建
-
第三部分 – 精炼和改进
-
-
为了充分利用这本书
-
联系我们
-
分享你的想法
-
-
第三维度的入门
-
本书的目标
-
转向 3D
-
坐标系
-
向量
-
相机
-
面、边、顶点和网格
-
材质、纹理和着色器
-
刚体物理
-
碰撞检测
-
-
Unity 界面
-
场景视图和层次结构
-
检查员
-
项目窗口
-
游戏视图
-
包管理器
-
-
Unity 基本概念
-
资产
-
场景
-
游戏对象
-
组件
-
脚本
-
预制体
-
包
-
-
总结
-
-
设计和原型
-
游戏设计基础
-
游戏设计文档
-
深思熟虑的决定
-
迭代生产
-
构思
-
-
你的第一个 Unity 项目
-
Unity Hub
-
选择版本
-
选择模板
-
可脚本化渲染管线
-
内置渲染
-
通用渲染
-
高清晰度渲染
-
-
-
原型设计
-
线框图或纸面创建
-
灰盒测试
-
概念验证 (PoC)
-
最小可行产品 (MVP)
-
垂直切片
-
-
总结
-
-
编程
-
设置环境
- Unity 环境
-
基础
-
变量
-
数据类型
-
Int
-
浮点数
-
字符串
-
GameObject
-
-
编程逻辑
-
if 语句
-
循环语句
-
循环语句
-
选择 for 和 while 的区别
-
-
方法
-
-
总结
-
-
字符
-
设计和概念
-
提问为什么
-
概念时间!
-
-
绑定
-
以动画为先的思考方式
-
变形
-
层次结构
-
骨骼或关节
-
正向运动学/逆向运动学
-
约束
-
变形器
-
控制
-
基于物理的动画
-
人类逆向运动学 (HIK) 系统
-
动画
-
-
角色控制器
-
内置角色控制器
-
刚体角色控制器
-
-
编写角色的移动脚本
-
Unity 中的初始设置
-
空闲状态
-
代码入口点
-
RequireComponent
-
更新代码
-
方法
-
-
总结
-
加入我们的 Discord!
-
-
环境
-
草图绘制
-
情绪板
-
舞台
-
-
排除它
-
Unity 地形
-
创建地形
-
地形设置
-
地形绘制
-
树木绘画
-
绘制细节
-
-
三维几何
-
ProBuilder
-
预制基本形状
-
-
-
总结
-
-
交互和机械
-
游戏循环
-
机械工具箱
-
资源管理
-
风险与回报
-
空间意识
-
集合
-
研究
-
局限性
-
-
设计和实现
-
我们的项目
-
楼梯
-
设计
-
实现
-
-
环形
-
设计
-
实现
-
-
狭窄空间
-
设计
-
实现
-
-
交互体积
-
设计
-
实现
-
-
-
总结
-
-
刚体与物理交互
-
刚体组件
-
质量
-
角动量拖拽
-
使用重力布尔值
-
是运动布尔值
-
插值
-
碰撞检测
-
连续
-
连续推测
-
-
约束
-
信息
-
-
心灵感应与物理交互
-
岩石坠落
-
设计
-
实现
-
-
破碎的基石
-
设计
-
实现
-
-
最后的谜题
-
设计
-
实现
-
-
-
摘要
-
-
用户界面和菜单
-
用户界面
-
叙事 – 叙事是,内部是
-
非叙事 – 叙事否,内部否
-
空间 – 叙事否,内部是
-
元 - 叙事是,内部否
-
-
UI 元素
-
主菜单
-
库存系统
-
健康表示
-
物品交互系统
-
-
我们项目中的 UI
-
主菜单
-
退出菜单
-
空间提示
-
-
Unity UI
-
Unity 画布系统
-
矩形变换
-
画布组件
-
画布缩放器
-
图形射线投射器组件
-
-
Unity UI 对象
-
实现
-
主菜单实现
-
日志实现
-
交互 UI 实现
-
-
-
摘要
-
-
视觉效果
-
视觉效果概述
-
着色器图
-
设置
-
创建
-
光照着色器图
-
精灵光照着色器图
-
精灵非光照着色器图
-
非光照着色器图
-
-
着色器图界面
-
主堆栈
-
黑板
-
图形检查器
-
主预览
-
节点
-
-
常用节点
-
添加
-
颜色
-
Lerp
-
乘法
-
样本纹理 2D
-
饱和度
-
分割
-
UV
-
向量
-
-
-
粒子系统
-
Shuriken
-
VFX 图
- 节点
-
-
总结
-
-
音效
-
声音…设计?
-
声音设计的五个要素
-
源
-
包络
-
攻击
-
发布
-
-
音调
-
频率
-
分层
-
-
设计可扩展性
-
我们项目的声音设计和实现
-
播放我们的第一个声音
-
组织
-
音乐
-
-
2D 声音
-
3D 声音
-
使用 3D 声音
-
音频监听器第一部分
-
-
3D 声音设置
-
音频监听器第二部分
-
将 3D 环境音添加到游戏中
-
填充我们的环境音
-
2D 环境音
-
-
-
通过玩家交互触发声音
-
通过 Unity 事件触发声音
-
旋转谜题声音
-
树形谜题
-
-
-
总结
-
-
构建和测试
-
使用 Unity 构建
-
目标平台
-
架构
-
服务器构建
-
复制 PDB 文件
-
创建 Visual Studio 解决方案
-
开发构建
-
自动连接分析器
-
深度分析支持
-
脚本调试
-
仅脚本构建
-
压缩方法
-
-
测试
-
功能测试
-
性能测试
-
Unity 分析器
-
内存分析器
-
帧调试器
-
物理调试器和分析器模块
-
-
游戏测试
-
浸泡测试
-
本地化测试
-
-
用户体验,或 UX
-
品牌
-
设计
-
可用性
-
初始问题
-
第一个谜题
-
介绍一个二级机制
-
最终谜题
-
-
总结
-
-
收尾工作
-
概述
-
资产最终化
-
风格化资产上的通行证
-
详细法线
-
架构清理
-
纹理混合
-
环境杂乱
-
详细网格
-
效果
-
楼梯阻挡器
-
飞镖系统 – 楼梯阻挡器粒子层
-
VFX 图 – Myvari 的意念移物
-
-
电影
-
二级动画
-
-
光照
-
3D 形状
-
提供情绪
-
游戏设计
-
Unity 光照
-
混合光照
-
光照探针
-
反射探针
-
-
-
声音润色
-
通过动画事件触发声音
-
使用事件为动画添加标签以产生声音
-
随机声音
-
随机音调
-
-
-
总结
-
-
奖励:其他 Unity 工具!
-
Unity 游戏服务
-
多人工具
-
创建
-
连接
-
沟通
-
-
XR 插件
-
机器学习代理
-
Bolt 可视化脚本
-
流程图
-
状态图
-
实时编辑
-
调试和分析
-
代码库兼容性
-
-
-
总结
-
-
你可能喜欢的其他书籍
-
索引
地标
-
封面
-
索引
第一章:第三维度的入门指南
欢迎加入!
很高兴您能加入我们,一起学习 3D 游戏开发的基础知识之旅。首先,我们将向您介绍编写这本书的团队。
-
特拉维斯·巴皮斯特(3D 艺术家)负责艺术指导,为游戏中的每个模型建模,为角色绑定,并帮助定义故事的设计。
-
拉塞尔·克雷格(高级软件工程师)负责编写游戏机制脚本。
-
瑞安·斯图克尔(声音设计师)在整个项目中创建并实现了所有声音。
-
安东尼·戴维斯(高级技术艺术家)撰写了本书,管理了项目,构建了效果和着色器,并完善了项目。
确保我们能够发挥我们 50 多年集体经验的最佳水平(这本书每一页背后都有 4 个大脑),每一天都是过山车般的体验(而且非常有趣!)我们花了超过六个月的时间,对整本书进行了两次修订(以及在这个过程中交换的数百个 GIF),以包含最合适的用例来解释新概念,最重要的是,提供一种有效的教学方法。最终,我们相信我们成功地创造了一本书,这本书将塑造我们在游戏开发领域的职业轨迹,并至少推动我们前进 3-5 年。
本书将为您提供开始构建所需的所有工具;然而,在将您的想法转化为作品的过程中,您可能需要更多的支持和建议。
这就是我们的 Discord 服务器发挥作用的地方。它为我们引入了互动元素,让我们能够一起阅读本书,并就您的 3D 游戏项目进行讨论。我在 Discord 上的时间比以往任何时候都要多,以确保您能够轻松地完成本书,所以请随时过来打个招呼,提出任何问题!
加入时,别忘了在#introduce-yourself频道中发个简短的自我介绍:packt.link/unity3dgamedev

好吧,让我们开始吧!
本书的目标
我们编写本书的目标是使每位读者都能够建立正确的思维方式来思考 3D 游戏,然后展示我们创建游戏所采取的所有步骤。即使是游戏开发领域的绝对初学者也可以通过这本书,尽管主题的难度可能会迅速增加。虽然困难,但如果您坚持下去,您将朝着游戏开发精通迈出多个步骤。本书的主要目标受众是那些在游戏开发方面有一定知识的人,尽管无论您的经验如何,我们都希望为您创造一个愉快的学习之旅。我们将涵盖的概念很快就会变得复杂,包括角色、编程、设计模式等,我们将学习这些内容。
为了最大限度地利用本书,我建议您遵循以下方法:
-
仔细阅读章节,故意停下来思考这些概念。
-
当某件事物全新时,检查我们的 GitHub 项目,看看观看它的实际操作是否能进一步解释它。如果不行,就去 Google 上自己研究。
-
如果项目中没有提供某些内容,请通过 Discord 发消息给我,或在社区服务器中寻求同伴的帮助——链接已在上文分享。
-
进入下一节并重复!
这种方法将使你能够掌握你感到困难的部分;一旦你完成了这个过程,你可以向同伴寻求帮助。你遇到的问题也可能被其他人遇到。解决这些问题并将它们带到 Discord 或让同伴帮助解决问题,可以增强整个社区的知识。
这本书是为了让你阅读我们的方法,然后查看项目以了解所有的基础。首先理解我们为什么这样做的设计是非常重要的。我们也花时间讲解 Unity 界面的基础知识,但技术可以通过在线的大量资源随着时间的推移而学习。
在这里你找不到的一些内容是如何建模角色、绑定或动画化它们。我们对这个过程讲得很少,因为那需要单独的训练。我们确实会解释为什么我们以这种方式设计角色,以帮助你走上同样的道路。项目包含了所有的动画和电影特效,所以最终产品可以让你看到我们工作的成果。这种方法是学习的一个强有力的方式,我们教你为什么事情会以这种方式完成。这样,你可以看到最终的结果,并且允许你发挥创意,对设计提出自己的想法,同时在工作过程中独立使用新工具,逐步通过章节。
最后,在我们深入内容之前,我们想建议你打开 GitHub 仓库,导航到Builds文件夹,亲自试玩。这将帮助你看到我们的小团队完整构建的内容。试玩后,你可以想象我们在构建这个项目的过程中经历了什么。
让我们深入探讨本章我们将涵盖哪些主题:
-
进入 3D 世界
-
必要的 Unity 概念
-
Unity 界面
让我们从熟悉 3D 游戏开发的基本组件开始。
进入 3D 世界
在本节中,我们将探讨 3D 工作的基本理解。从坐标系统到 3D 模型的渲染构成,我们只会进行表面层的讲解,以确保你在这一旅程中完全理解基础。通过阅读这些内容,你将获得 Unity 如何显示物品的强烈理解。
坐标系统
3D 坐标系在每种 3D 应用程序中并不完全相同!如 图 1.1 所示,Unity 是一个左手坐标系,+y 方向向上。看着 图 1.1,你可以可视化左手系和右手系之间的差异。

图 1.1:坐标系
当我们在这些坐标系中工作时,你会看到物体的位置以括号内的三个值组成的数组形式表示,如下所示:
(0, 100, 0)
这分别代表 (x, y, z)。这是一个好习惯,因为编程在脚本中编写位置时使用非常相似的语法。当我们谈论位置时,通常指的是你使用的任何 数字内容创建者 (DCC) 中的 transform。在 Unity 中,transform 包含位置、旋转和缩放。
现在我们理解了世界坐标 (x, y, z),以及这些坐标每个都是从 0 开始,由 (0, 0, 0) 表示。在下面的 图 1.2 中,彩色线条相交的地方在世界上是 (0, 0, 0)。立方体有自己的 transform,它包含该对象的位置、旋转和缩放。记住,transform 包含局部位置、旋转和缩放。世界 transform 是从这个基础上根据其层次结构计算出来的。

图 1.2:3D 坐标系
图 1.2 中的立方体位于 (1, 1.5, 2)。这被称为 世界空间,因为项目的 transform 是通过从 (0, 0, 0) 开始的世界坐标来表示的。
图 1.3:世界空间与局部空间
现在我们知道立方体的 transform 是相对于世界 (0, 0, 0) 的,我们将讨论描述局部空间的父子关系。在上面的 图 1.3 中,球体是立方体的子项。球体的局部位置是相对于立方体的 (0, 1, 0)。有趣的是,如果你现在移动立方体,球体将跟随,因为它只是从立方体偏移,并且其 transform 将相对于立方体保持 (0, 1, 0)。
向量
传统上,向量是一个具有多个元素和方向的单位。在 3D 环境中,Vector3 将与我们迄今为止所使用的内容非常相似。(0, 0, 0) 就是一个 Vector3!向量被用于许多游戏元素和逻辑的解决方案中。通常,开发者会规范化向量,这样其大小始终等于 1。这允许开发者轻松地处理数据,因为 0 是起点,0.5 是中间点,1 是向量的终点。
摄像机
摄像机是极其有用的组件!它们谦逊地展示它们的视角,这允许我们的玩家体验我们试图传达给他们的内容。正如你可能已经猜到的,摄像机也像所有 GameObject(我们将在本章后面描述)一样有一个 transform。摄像机还有几个可以更改的参数,以获得不同的视觉效果。
不同的游戏元素和类型使用相机的方式不同。例如,游戏 生化危机 使用静态相机来营造紧张感,不知道窗外或拐角处有什么,而 古墓丽影 在玩家角色劳拉穿过洞穴时将相机拉近,给人一种亲密感和情感理解,她的脸在狭窄的空间中显得不舒服。
相机对于你将为用户创建的体验至关重要。花时间玩弄它们,并学习构图概念,以最大化玩家体验中的情感推动。
面、边、顶点和网格
3D 对象由多个部分组成,如 图 1.4 所示。顶点,用绿色圆圈表示,是相对于世界 (0, 0, 0) 的空间中的点。每个对象都有一个顶点的列表及其相应的连接。
两个顶点相连形成一个边,用红线表示。当三个或四个边连接形成一个三角形或四边形时,就形成了一个面。有时当四边形没有连接到其他任何面时,它们被称为平面。当所有这些部分组合在一起时,你就有了网格。

图 1.4:顶点、边、面和网格
材料、纹理和着色器
现在你已经知道了在所有 DCC 工具中网格由什么组成,让我们来看看 Unity 如何将网格显示给你。在基础级别是着色器。着色器可以被视为小程序,它们有自己的语言并在 GPU 上运行,因此 Unity 可以在你的屏幕上渲染场景中的对象。你可以把着色器想象成创建材料的模板。
更高级的是材料。材料是一组由着色器定义的属性,用于操作,有助于展示对象的外观。每个渲染管线都将有单独的着色器:内置、通用渲染管线(URP)或高清渲染管线。对于这本书,我们使用第二种选项,也是最广泛使用的:URP。
图 1.5 展示了使用 URP 的 标准光照 着色器的材料示例。这允许我们操作表面选项、该表面的输入以及一些高级选项。现在,让我们只谈谈 基础图,这是 表面输入 部分中的第一个项目。在这里使用 基础图 是将 漫反射/阿尔贝托 和 着色 结合起来。漫反射/阿尔贝托 用于定义将应用于表面的基础颜色(红色)——在这种情况下,白色。
如果你通过将纹理拖放到基础地图(绿色)左侧的方块或点击方块和名称之间的圆形(蓝色)将纹理放入此图,之后,你可以根据需要调整颜色来着色表面。

图 1.5:基础材料属性
图 1.6 展示了一个简单的例子,展示了立方体在着色、纹理以及纹理着色改变后的样子。随着我们阅读本书的深入,我们将解锁更多关于材质、着色器和纹理的功能。

图 1.6:着色和纹理基础颜色
纹理可以为你的 3D 模型提供惊人的细节。
在创建纹理时,分辨率是一个重要的考虑因素。需要理解分辨率的第一部分是“2 的幂”大小。2 的幂如下所示:
2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096 等。
这些数字代表宽度和高度的像素大小。在某些情况下,你可能需要混合大小,只要它们符合 2 的幂比例即可。例如:
-
256×256
-
1024×1024
-
256×1024(这种情况较少见,但也是有效的)
关于分辨率的第二个考虑因素是大小本身。处理这个考虑因素的最简单方法是考虑 3D 对象在你的屏幕上的大小。如果你有一个 1920x1080 的屏幕分辨率,那么它有 1920 像素宽和 1080 像素高。如果所讨论的对象只占据屏幕的 10%,并且很少被看到更近的距离,你可能考虑一个 256x256 的纹理。相比之下,如果你正在制作一个情感驱动、以角色为中心的游戏,其中情感和面部表情很重要,那么在这些场景中,你可能想在面部使用 4096x4096 或 4K 纹理。
刚体物理
Unity 假设每个 GameObject 不需要在每一帧都进行物理评估。Unity 使用 Nvidia 的 PhysX 引擎进行物理模拟。为了获得任何计算出的物理响应,GameObject 需要添加刚体组件。
通过将刚体组件添加到 GameObject 中,你将在下面的 图 1.7 中看到的一些 GameObject 属性将出现在检查器中。

图 1.7:刚体
一个 Unity 单位的质量等于 1 千克的质量。这会影响碰撞时的物理决策。阻力 单位增加摩擦,随着时间的推移减少速度。角阻力 类似,但仅限于旋转速度。使用重力 可以打开或关闭重力,等于标准地球重力 (0, -9.81, 0),这样质量才有意义!有时你可能不想使用地球重力,因此你可以更改物理设置以使重力符合你的需求。
在 第七章,刚体和物理交互 中,我们将详细解释刚体的概念。我们将在创建角色、环境以及交互式游戏玩法时使用刚体。
碰撞检测
没有任何碰撞器的 Rigidbody GameObject 将无法充分利用物理特性,当开启重力时,它将直接穿过世界。有相当多的碰撞器可供选择,以最好地满足您游戏的需求。在下面的图 1.8中,您可以看到为 2D 提供的独立碰撞器。这些使用与 3D 不同的物理系统。如果您只为游戏使用 2D,请确保使用 2D 碰撞器。

图 1.8:碰撞器组件选项
您也可以添加多个碰撞器——如上图中图 1.8所示的基本选项——以最好地适应 GameObject 的形状。在空 GameObject 上看到碰撞器是很常见的,这些 GameObject 是主对象的子对象,以便于轻松变换碰撞器。我们将在第四章,角色和第五章,环境中看到这一点。
Unity 界面
Unity 的界面被分为几个主要组件。在下图的图 1.9中,我们将介绍场景(红色)及其界面中的项目,以及如何在检查器(橙色)中操纵它们的属性。然后我们将讨论场景中未激活但可在项目窗口中添加的项目(黄色)。最后,我们将介绍游戏视图(绿色)和包管理器(与图 1.9分开)。

图 1.9:整体界面
场景视图和层次结构
场景视图和层次结构协同工作。层次结构是游戏运行时场景的渲染方式。场景视图允许您实时操纵 GameObject 及其值。此外,当编辑器处于播放模式时,游戏可以对层次结构中的 GameObject 进行更改。
当 GameObject 在播放模式下被操纵时,包括您在场景视图中自行更改它们,在停止游戏后,GameObject 将恢复到游戏开始前的原始状态。

图 1.10:场景和层次结构
在上图的图 1.10中,您可以立即看到很多信息。在左侧的层次结构中,您可以看到场景中的对象。这些对象都有一个transform,它们将对象放置在世界上。如果您双击一个项目或单击一个项目,将鼠标放在场景视图中,然后按f,您将聚焦于那个 GameObject,这将使项目在场景视口的中心。
当您选择一个项目时,您可以看到在对象的旋转中心点——通常是对象的中心——有一个显示彩色箭头的工具。该工具允许您在空间中定位 GameObject。您还可以通过选择两个轴之间的一个小正方形来在平面上定位对象。
在图 1.10的右上角,你会看到一个相机 Gizmo。这个小 Gizmo 将允许你通过单次点击轻松地将视口相机定位到前面、侧面、顶部、底部,或者将其更改为等轴测相机或透视视图。
现在你已经看到了场景中的项目,通过在场景或层次结构中左键单击选择,你可能想要更改一些属性或向该 GameObject 添加组件。这就是检查器发挥作用的地方。
检查器
要操纵 GameObject 的值,当你选择场景或层次结构中的 GameObject 时,检查器将更新以显示每个 GameObject 可用的更改选项。
![img/B17304_01_13.png]
图 1.11:检查器窗口
图 1.11中的检查器窗口显示已经选择了大量此类项目。在顶部,名称是Cube,左侧的蓝色立方体表示预制件数据类型。你可以通过点击名称下方仅有的打开按钮来更改预制件本身。这将创建一个新的场景视图,仅显示预制件。当你更改预制件时,它将更改任何引用它的场景中所有实例化预制件的设置。
变换组件显示了场景中预制件的位置、旋转和缩放。
网格过滤器显示了构成该多边形的顶点、边和面。
在下面是网格渲染器。此组件将允许渲染网格过滤器组件中渲染的网格。我们可以在这里设置材质以及其他与该项目的特定光照和探针相关的选项,这些将在第十二章,最终润色中介绍。
现在,下面是一个碰撞器和 Rigidbody。这些组件协同工作,帮助该对象根据组件上的设置实时响应物理效果。
我们已经讨论了很多场景中的项目及其属性,但它们如果只是引用项目,那么在场景之外它们存储在哪里?项目窗口将回答这个问题。
项目窗口
在这里,你可以找到将在场景中实例化或在游戏中作为组件使用以完全实现你正在构建的游戏的资产。
![img/B17304_01_14.png]
图 1.12:项目窗口
此窗口是引用的 GameObject 的物理表示。在图 1.12中看到的资产文件夹中的所有项目都物理地存储在你的硬盘上。Unity 会创建元文件来存储这些项目的所有属性。
在项目窗口中保留原始文件的好处是,你可以更改项目,并且当你聚焦于 Unity 项目(点击 Unity 应用)时,它将重新调整元文件并重新加载场景中的项目。这使得你可以更快地迭代脚本和艺术作品!
我们已经查看场景中的 GameObject,通过操作变换来放置它们,并知道 GameObject 的引用位置。现在我们应该查看游戏视图,以了解游戏本身的外观。
游戏视图
游戏视图类似于场景视图;然而,它遵循场景视图内构建的规则。除非您定义了一个不同的相机进行渲染,否则游戏将通过主相机自动渲染场景内容。

图 1.13:游戏视图
您可以看到这看起来非常类似于场景窗口,但顶部有不同的选项。在左上角,我们可以看到显示下拉菜单。这允许我们在场景中有多个相机时更改相机。比例位于其右侧,这有助于查看以针对特定设备。屏幕比例右侧的缩放有助于快速调整窗口大小或放大以进行调试。播放时最大化将在播放时最大化屏幕以充分利用全屏。静音音频将静音游戏音频。统计信息将在游戏视图中提供一个小概览。
在本项目的后期,在优化过程中,我们将通过更深入的分析来查看可能影响游戏玩法中内存使用和其他优化机会的问题。

图 1.14:游戏统计信息
继续向右是Gizmos。这是一组在图 1.14中游戏视图中显示的项目。根据您的需求,您可以在该菜单中打开或关闭它们。
包管理器
您的 Unity ID 将存储您从 Unity 资产商店购买的包以及您可能已在硬盘或 GitHub 上的包!您可以使用包管理器将包导入到项目中。
您可以在窗口 > 包管理器下找到这些包,如下面的图 1.15所示。

图 1.15:包管理器路径
打开包管理器后,您最初会看到项目中包含哪些包。您可以通过更改左上角的下拉菜单来查看 Unity 中的标准内容或您在 Unity 资产商店购买的包。

图 1.16:包管理器
通过选择Unity 注册表,您将看到一系列由 Unity 测试的免费包,它们是 Unity 平台的一部分,如果您需要它们,则可用。您可以通过点击左侧的包并查看右侧带有查看文档标签的链接提供的文档来了解每个包。
如果您选择项目内,它将显示当前加载的项目中已安装的包。当您想要卸载可能不需要的包时,这很有帮助。
我的资产是您购买或正在进行的项目的资产以及与您的 Unity ID 相关的之前付费的资产。
内置是任何项目的标准配置。您可能需要根据您的需求启用或禁用内置包。探索它们并禁用不需要的包;一个整洁的项目现在将导致以后的优化更少。
必要的 Unity 概念
在第一部分,我们已经介绍了一些 Unity 的概念。在这里,我们将更详细地讲解它们,因为您之前已经阅读过,其中一些可能会被用到。Unity 对游戏开发环境中的项目有着非常模块化的关注。
资产
Unity 将每个文件都视为一个资产;包括 3D 模型、纹理文件、精灵、粒子系统等等。在您的项目中,您将有一个Assets文件夹作为基础文件夹来存放所有项目项。这些可能包括纹理、3D 模型、粒子系统、材质、着色器、动画、精灵等等。随着我们向项目中添加更多内容,Assets文件夹应该是有序的,并准备好扩展。强烈建议您保持文件夹结构有序,这样您或您的团队就不会浪费时间试图找到那个不小心被遗留在随机文件夹中的纹理项。
场景
一个场景包含了所有的游戏逻辑、游戏对象、电影场景以及游戏中将引用以渲染或与之交互的所有其他内容。
场景也被用来分割游戏部分以降低加载时间。如果您想象一下每次加载现代游戏时都要加载所有单个资产,这将占用太多的宝贵游戏时间。
游戏对象
在场景中引用的大多数资产都将是一个GameObject(GO)。在某些情况下,一个资产只能是一个 GO 的组件。所有 GO 的共同因素是它们都有Transform组件。正如我们在本章开头所读到的,transform包含局部位置、旋转和缩放。世界变换是从这个基础上根据它们的层次结构计算出来的。GOs 可以连接一个长长的组件列表,以提供功能或数据,用于在脚本中实现机制的增长。
组件
GOs(游戏对象)能够容纳多个附加的功能组件。每个组件都有其独特的属性。您可以在下面的图 1.17中看到,您可以添加的组件列表相当广泛。

图 1.17:组件列表
这些每个部分都有更小的子部分。在这本书中,我们将详细讲解其中的一些。当您向场景层次结构中添加需要组件的资产时,Unity 会默认添加它们。一个这种默认操作发生的例子是,当您将一个 3D 网格拖入层次结构时,GOs 会自动附加一个网格渲染器组件。
脚本
在 GameObject 上经常使用的一个组件是脚本。这是所有逻辑和机制将被构建到您的 GameObject 上的地方。无论您是想改变颜色、跳跃、改变白天的时间,还是收集物品,您都需要在对象上的脚本中添加该逻辑。
在 Unity 中,主要使用的语言是C#(发音为“C sharp”)。这是一种强类型编程语言,意味着任何被操作的变量都必须有一个类型分配。
我们将以多种方式使用脚本,我知道你迫不及待地想要开始编码,但首先,我们需要了解其他 Unity 标准流程。
预制件
利用 Unity 模块化和强面向对象的特点,我们可以将一组具有默认组件值的物品组合在一起,这些物品可以在场景中的任何时间实例化,并拥有它们自己的值。
要创建一个预制件,你只需将场景中的 GameObject 从层次结构拖动到资产浏览器中。它将创建一个新的预制件,并将该 GameObject 转换为新创建的预制件。默认情况下,它将在层次结构中变为蓝色,如图1.18所示。

图 1.18:层次结构中的预制件
包
为了将模块化组件提升到全新的水平,Unity 可以将包含所有依赖关系的包导出,以便你将其带入其他项目中!更好的是,你还可以通过 Unity Asset Store 将你的包卖给其他游戏开发者!
现在你已经对 3D 和 Unity 术语有了坚实的基础,让我们打开它并回顾界面本身。下一节将探讨 Unity 所有最常见界面组件。
概述
我们一起回顾了几个关键领域,以开始你的游戏开发之旅。在本章中,我们通过探讨三个主要主题的一些基本特性,为即将到来的内容奠定了基础。对于三维空间,我们回顾了坐标系、向量、摄像机、3D 网格以及 Rigidbody 物理和碰撞检测的基础。这些基础知识足以让我们进入 Unity 的概念,例如资源和 GameObject,然后是 C#脚本编写和预制件基础。为了结束本章,我们进行了一次 Unity 界面的虚拟游览——场景、层次结构、检查器和包管理器。
在下一章中,我们将探讨设计和原型设计的基础知识。这将使我们能够在描述本书中创建的项目时,跟随我们的思维过程。它还将为你创建自己的项目奠定基础知识,在你完成本书后继续使用。
第二章:设计和原型
现在我们已经了解了游戏开发的全部主要术语,并对 3D 空间有了更深入的理解,我们需要讨论游戏本身。在这本书中,我们正在构建一个垂直切片——游戏的一个完全功能的部分。对于这一章,我们将进入项目启动的初期。主要话题包括:
-
游戏设计基础
-
您的第一个 Unity 项目
-
原型设计
首先,让我们从顶部开始,更详细地讨论游戏设计基础。花点时间阅读这部分内容,因为它充满了知识要点,将帮助你将游戏提升到下一个层次。
游戏设计基础
游戏设计是一门年轻的技艺。在艺术中,有一些非常基本的原则必须在探索之前考虑。我们将探讨开发者如何喜欢在“文档”中捕捉他们的想法。然后,我们将从如何尽可能细致地故意做出每个决策的微观讲座开始。然后,我们将通过迭代构建这些决策。最后,我们将解释概念化。让我们从设计文档的讨论开始。
游戏设计文档
曾经有一段时间,我们的团队在开发冲刺之间有一段空闲时间,我们所有人都想尝试一个新的工具。我们的项目管理正在使用 Atlassian 套件(Jira,Confluence 等),但我们想看看还有什么更好的选择,所以我们查阅了多种不同的软件。这包括 Hack N’ Plan,Trello,Notion 和其他一些工具。我们使用所有这些工具来查看哪些会在我们的休息之后被使用。最终,我们发现我们喜欢 Jira 用于项目管理任务,但对于其他所有事情,我们坚持使用 Miro。Miro 最终成为了我们的概念板和设计/工作流程头脑风暴工具。这是通过其他工具没有被团队大多数成员使用而自然发生的。
无论你的游戏看起来有多小,都可能会有某种形式的文档需要制作。制作文档有很强的组织原因,但最强大的原因是当我们把某事写在纸上或在协作空间中绘制出来时,我们往往会花更多的时间来认真考虑其优点。这种暂停有时被称为启发式设计。这可以单独完成或协作完成。
一些设计师希望在文字处理软件或在线协作工具中绘制一份书写精美、结构清晰的文档。这提供了一个整洁的概要,并能够精确地写出每个细节。当游戏范围较大时,这种方法效果很好。作者往往是技术作家,精通文档化流程的艺术。这种方法是确保游戏的任何部分都有一个单一的真实来源,任何人都可以在开发过程中参考,但这样做可能不是对你或你的团队最好的方法。
另一种制作游戏设计文档的选项是通过协作头脑风暴软件。这允许用户以创造性的方式一起工作,制作流程图、绘图和概要。这种方式是上述书面文档方法精心策划形式的直接对立面,但满足不同的需求。创造性的形式往往更加亲密和艺术化。一些预概念草图被绘制出来,快速绘制流程图以提出关于游戏元素的问题,想法可以迅速被采纳、丢弃或保留。这种方式的设计对于大型团队来说可能不太适用,因为没有真正的组织结构。新成员在这些情况下可能会有困难融入。

图 2.1:流程图示例
这两种选择都不是制作游戏设计文档的灵丹妙药,但请放心,你的团队需要某种形式的文档来记录你的想法。想法在脑海中是短暂的,一些最好的想法如果没有被记录下来保存,就会消失在虚空中。与你的团队进行实验,找到最适合他们的选项。设计团队中有一句俗语:“最好的工具是团队真正会使用的工具”。
我们已经讨论了许多游戏设计文档的选项,并展示了它们的优缺点。尽管一开始并没有完美的方法,但一个很好的起点是从更视觉化和协作的方法开始。如果你在一起,这可能是一个干擦板和便利贴。干擦板允许非永久性的思考,而便利贴则是需要完成的任务。将它们放在左侧表示“需要完成”,当它们完成时,将它们移动到右侧。
我建议你花些时间在我们的 GitHub 仓库中查看我们为这本书创建的仓库。我在那里为你添加了一个GDD 图像文件夹,你可以查看大量示例,看看我们将在下一组章节中处理的内容。
github.com/PacktPublishing/Unity-3D-Game-Development
现在我们已经开始记录我们的游戏设计,我们需要对我们的想法进行深思熟虑的选择,使它们具体化。
故意决策
尽管本章的这一部分可能比其他部分略短一些,但请将这一节铭记在心:成为一名设计师意味着构建一个即使在没有意义时也具有沉浸感的虚拟世界。玩家会无意识地以惊人的速度进行观察。玩家能看到的与游戏环境或角色不协调的拼图碎片越多,沉浸感就会越被破坏。解决任何破坏沉浸感问题的最佳方式是做出深思熟虑的决定。
为了给出一个非常简单的解释,以门把手为例。你一生中见过它们,并本能地使用它们。事实上,当你不得不处理一个设计糟糕的门把手时,你真正的、现实生活中的沉浸感就会被破坏。如果你曾经抓住门把手试图拉门,却发现门是设计成要推的,你就遇到了这个问题。如果门只允许向一个方向移动,那么出口的正确设计就是一个门把手所在位置的平面板。这立即暗示了“推”。
每个级别、网格、纹理、概念和感觉都需要经过深思熟虑并尝试实施。只有在你有充分的理由将某物放置在特定位置而不屈服于陈词滥调时,你才能探索其他独特的方面,从而构建真正独特的东西。
在本书中,你将制作并与之互动的项目已经经过了长时间的深思熟虑。为了强调这一点,在每个部分,你都会看到一系列问题,这些问题将以简洁的方式尽可能详细地回答。
迭代生产
游戏开发对沉浸感的需求非常有趣,需要将其置于游戏体验的前沿。为了使沉浸感尽可能完整,开发团队需要不断询问当前的方向是否运作良好。非常常见的情况是,你开始开发的游戏最终可能并非你所期望的那样。这个周期被称为迭代设计或生产。
当你进行迭代设计时,你可以使用多种模式。这里将要描述的方法不是完成设计的唯一确定方法,但它是一个良好的起点,你的团队可以从这里根据需要分支发展。
游戏要想在易于理解方面得到成长,迭代需要频繁且尽早进行。有一个概念叫做MVP,即最小可行产品,游戏开发者制作出所需的最小游戏元素以供测试者测试。这应该花费很少的时间,并且反馈是无价的。当你将这个版本交给测试者时,你会得到一些反馈,这些反馈是你和你的团队无法看到的,因为你们非常接近产品。确保以开放的心态仔细倾听反馈,因为他们的经验可能在你的玩家中很常见。我们正在努力为尽可能多的玩家设计一个精心设计的体验。这种反馈迫使你和你的团队对设计进行迭代,并可能裁剪或添加游戏机制,以应对主要的测试反馈。


图 2.2 a, b:关卡设计中迭代的示例
在迭代解决了你设计中的主要漏洞之后,你然后进入游戏的垂直切片(将在本章的垂直切片部分中介绍)。这应该是一个你对自己在运动和主要游戏机制方面的基本感到舒适的迭代。你的团队将希望从开始到结束制作一个完整的游戏循环,使用一个包含胜利和失败条件的单个关卡。然后,你猜对了,再次测试,但这次是与从未见过这个游戏的新测试者。提出类似的问题,以及在内测期间出现的一些新问题。
开发循环应该看起来是重复的,它确实是这样的:
-
思考和测试
-
创建和测试
-
更新和测试
然后,继续这种做法,直到你达到可以发布的产品的迭代次数。每个步骤最重要的部分是测试。确保将测试反馈作为改进所需的地方的强烈指示。我们将从这个概念化阶段开始这个周期。
构思
你需要制作一个游戏,并且你已经有一个准备就绪的团队。你习惯于有意识地做出细致的决策,并且了解迭代过程。现在你需要开始一个概念。
开始一个项目的第一步是探索你和你的团队希望玩家体验到的情感。鉴于我们的艺术形式如此年轻且可塑,我们可以以任何我们喜欢的方式追求这种情感。这是游戏开发者的力量。一旦你知道你关注的情感是关于玩家将体验到的体验,就开始思考你如何将其作为游戏体验来创造。
如果情感是恐惧,你可以让玩家处理黑暗的空间,只有手电筒作为他们的主要防御工具。这可能会让你探索声音设计作为你的开发重点,因为视觉将不会是主要体验工具。
如果情绪是悲伤,那么你可能可以通过叙事焦点来处理,在这个焦点中,你扮演一个失去家庭成员的孩子,玩家们在一个梦境世界中通过叙事驱动的游戏玩法来处理故事。这推动了叙事和节奏,通过儿童视角对色彩理论和悲伤阶段有紧密的理解。
关于概念,我们可以继续讨论,因为存在无限多的场景。选择你的主要目标,然后朝着这个目标努力。在此之后,你可能想要将这些想法写下来,以了解沉浸感的感觉,从艺术的角度来看。这可能是角色概念轮廓。也可能是建筑设计。
这也可能是一组你保存的图片,这些图片给你带来了想要唤起的情感,你可以从中获取灵感。



图 2.3 a, b, c:项目中使用的概念
无论哪种方式,突出的行动是视觉上开始这些想法。在你绘制了一些艺术画板并有了如何构建的视觉想法之后,我们然后创建一个 Unity 项目。
你的第一个 Unity 项目
你已经整理了一个想要开发的概念,现在我们需要获取 Unity 并创建一个项目。为此,我们需要获取 Unity Hub 并选择一个版本,然后选择一个模板开始。
Unity Hub
Unity Hub 是一个小型应用程序,它将所有项目集中存储在一个位置,因此你可以轻松访问所有项目以及你安装的 Unity 版本。要获取 Unity Hub,你需要访问unity.com并创建一个UnityID。在创建账户后,点击名为开始的蓝色按钮。根据你的需求和操作系统,遵循最合适的提示。下载并安装Unity Hub,然后开始创作!
选择版本
Unity 可以同时运行多个版本。有Alpha、Beta、Official和LTS发布版本。
Alpha 版本具有实验性功能,可能不完全完整或生产就绪,不建议用于构建,因为可能存在导致构建中断的错误。工作室和爱好者可以使用此版本进行测试机制、引擎功能或包。它们通常比官方发布提前一个版本。Beta 版本与 Alpha 版本类似;然而,它们是最当前官方发布的实验版本。官方发布是稳定的当前版本。LTS 代表长期支持。这些是版本的最终发布版本,如果发现错误,将包含一些小型的热修复。
通过Unity Hub可以轻松查看版本。图 2.4展示了其可能的样子:

图 2.4:Unity 版本示例列表
对于生产应用,建议使用 LTS 版本。如果你的团队正在尝试或想要使用新功能进行原型设计,那么只能在预发布版本中实现。在为项目选择要构建的版本后,你需要在创建新项目时从 Unity 的选项中选择一个模板。
然而,这本书是版本无关的。如果你在 2022 年之后购买了这本书,这本书仍然具有相关性。截图可能会有轻微的 UI 变化,但基础仍然保持不变。
选择模板
当你在项目标签页上按下新建按钮时,Unity 会为你提供一些模板选择。这些选项包括2D、3D、通用渲染管线(URP)和高清渲染管线(HDRP)。这些模板之间有显著的渲染差异,以及一些可能对你和你的团队来说很有趣的功能。这些模板之间的差异是在可脚本渲染管线(SRP)出现时产生的!
可脚本渲染管线
渲染和计算机图形学是一个可以攻读博士学位的详细主题,因此我们将通过渲染管线探讨一下可能实现的内容。管线的顶层包括三个任务:剔除、渲染和后期处理。在这些类别中,许多任务按照特定的顺序和精度进行。所有这些的主要功能是优化视图,以便以高帧率向最终用户提供,同时保持用户期望的艺术风格。
随着 SRP 的出现,这些模板分为三大类:内置、通用和高清。为了更好地理解这三种模板,让我们将它们分别归类并进一步探讨。对于我们的项目,我们将使用通用渲染,因为我们将在渲染管线中利用多个功能。
内置渲染
这是一个较旧的管线,它不使用可脚本管线。内置渲染器有许多应用。2D 和 3D 模板都运行内置渲染系统。这也是在 SRP 出现之前,资产商店中大多数资产所采用的标准。你可以将“内置”视为 Unity 中的基础体验。有多个原因你可能不想使用内置渲染器。如果你想要使用体积光照、GPU 粒子或光线追踪,你将需要查看下面的可脚本渲染管线。
通用渲染
通用渲染管线(Universal Rendering Pipeline,简称 URP)恰如其名,因为它提供了最丰富的功能,并具有可脚本化的渲染管线。如果你想要制作 2D 游戏,这是最佳选择,因为它具有内置的像素级渲染、2D 光源和 2D 阴影。对于 3D 选项,这也是一个极好的选择。URP 和 HDRP 都提供了两个图表,分别是 ShaderGraph 和 VFXGraph。ShaderGraph 是一个视觉着色器创建工具,允许以视觉方式编写复杂的着色器。VFXGraph 的主要功能是作为一个专注于 GPU 粒子的粒子系统,允许你在屏幕上同时创建数百万个粒子,以实现惊人的视觉效果。
我们希望在项目中使用基于 GPU 的粒子,由 VFXGraph 负责处理,以及展示 ShaderGraph 的使用。有了这些要求,我们选择在 URP 中工作,作为我们的渲染管线。
如果你正在寻找一个更物理准确的渲染系统,具有光线追踪和体积云,那么 HDRP 正是你所需要的。
高清渲染
这个渲染管线有一个主要目的:在尽可能优化的同时,提供最佳的外观输出。是否使用 HDRP 是一个广泛讨论的话题。有几个主要原因表明 HDRP 可能是你的选择。这是如果你在寻找基于物理的天空,具有云层、体积云、多个方向光源、高度可定制的阴影选项,以及包括光线追踪反射、体积和多个高级着色器输出的光线追踪。HDRP 还可以提供许多其他高级渲染选项。这些概念是计算机图形世界中的深度话题,我们强烈建议你查找它们,以了解实时渲染正在成为什么样的美丽工作。
原型设计
现在你有了项目,你可以开始收集将创建游戏的资源。在这本书中,我们已经探讨了如何构建这个游戏,以便我们可以将每个主要部分划分到章节中。原型设计可以通过多种方式实现。我们无法详述每个工作室的原型设计方式,因为每个业务都有其独特的创作方式。我们将讨论整个行业中普遍存在的重大进展模式。将任何基于迭代的任务的生命周期分解,需要有一个循环来去除杂质。这通常被认为是分析、设计、实施和测试,然后迭代直到完成。原型设计阶段也要经过所有这些步骤。看看所有这些,并处理对你或你所在团队构建游戏有意义的每个部分。
线框图或纸面设计
在这种原型设计形式中,创作者将视频游戏分解为物理或数字系统中的阶段,以通过每个游戏循环或体验玩家在整个游戏中的感受。有时,这可能意味着创建一个纸板游戏来运行规则。有时,这可能涉及通过用户界面进行数字绘制游戏线框图,以直观地体验游戏玩法。
灰盒
这个名字就是你所想的含义!一堆未着色的形状,通常是灰色盒子,它们勾勒出你的环境,以确保可以通过其轮廓定义环境的叙事。这种原型设计版本特别有用,如果你需要一个非常直接的摄像机角度来展示,并且没有设置环境的资产。这也可以在概念艺术的发展中很有用,因为你可以将构图推给概念艺术家以获得更快的周转。


图 2.5 a, b:项目中的灰盒示例
从上面的图 2.5中,你可以看到概念艺术家如何在这些基础上绘制,以获得渲染的概念,从而为空间提供更多的设计想法,即使这意味着改变环境,因为这是一个快速的空间草图,以开始工作。
这些可以提供足够的细节来开始工作于一个概念验证,我们将在下一部分进行讨论。
概念验证 (PoC)
命名相当准确。这是你开始非常具体地进行测试的地方。你可能需要调整摄像机以获得对游戏玩法的非常具体的感受。这可能需要自身多次迭代,如果有一个团队,可能需要多个人尝试。


图 2.6 a, b:游戏资产迭代的示例
图 2.6 a 和 b 上方展示了某些建筑的迭代。我们从一个简单的拱门开始,它有一种幻想的感觉,只是为了开始。在我们将其放入关卡后,我们有更多的时间来思考风格,并为拱门添加更多吸引人的部分。这是一个有用的概念,了解你的资产在开始时可能并不完美。要达到 MVP,你必须从某个地方开始,朝着伟大努力!
最小可行产品 (MVP)
这正如其名所暗示的那样:游戏的简化版。在平台游戏中,必须包含跳跃。也许你的游戏需要作为机制,有摇摆动作,那么无论你有多少资金,这个机制都不会被删减?你不需要精美的艺术资产,甚至不需要动画。最小可行产品(MVP)的目的是展示游戏玩法特性在可接受的范围内,以确保在 MVP 机制的基础上构建的任何内容都能正常工作。
垂直切片
有时候你对艺术方向、主要机制和叙事有很好的想法,但需要收集一些反馈或可能需要资金。垂直切片是指你从游戏中取一个非常薄的切片并对其进行润色,以产生炒作和最终产品的感觉。演示与垂直切片有相似的概念。这比 MVP 更复杂,因为艺术、动画、机制、照明等方面的润色是 MVP 所期望的,这需要大量的时间和对最终产品的理解,而这些可能在制作 MVP 时可能还没有可用。
这种原型制作风格是本书中我们项目需求的最佳用例。我们正在开发游戏的一小部分,以深入了解整个游戏可能是什么样子。这是我们最好的选择。
在制作原型时,你的游戏可能需要经历所有这些步骤才能达到令人愉悦的游戏体验。你可能也不需要所有这些。这在不同的发展团队中差异很大。
摘要
在本章中,我们讨论了一些深入的主题——游戏设计、为你的第一个项目选择哪些选项,以及原型制作的基础。我们讨论了你和你的团队如何在一个 Word 文档或更直观的流程图中协作,将你的游戏整合起来。这里的想法是将设计的想法变为现实。一旦你做到了这一点,你应该深入到你的第一个 Unity 项目中,选择要使用的模板,并利用符合你游戏概念设置的特效,例如 GPU 粒子。最后,我们讨论了原型制作,以启动你的项目并了解它是否传达了你希望传达给用户的体验。
在下一章中,我们将开始探讨编程,帮助你将所有游戏想法变为现实。
第三章:编程
欢迎来到第三章!我们将涵盖 C#的所有基础知识以及如何在 Unity 中使用它。我们将回顾大多数项目所需的编程知识的主要部分。当我们在每个未来的章节中进行脚本编写时,本章应作为可参考的章节。我们首先需要确保您的计算机环境已设置好,以便开始使用 Unity 进行编程,然后进入编程的基础知识。本章将作为您 Unity 项目的基石。包括以下主题:
-
设置环境
-
变量
-
数据类型
-
编程逻辑
-
方法
设置环境
编程环境特指您将使用的集成开发环境(IDE)以及与之相关的依赖项。C#是微软.NET 框架的一部分,需要在您的机器上安装才能工作。幸运的是,对于世界上的许多 IDE 来说,当您开始用 C#工作时会为您安装它。更幸运的是,如果您从 Unity Hub 安装,Visual Studio 将预先配置好,您可以立即开始开发您的项目!让我们一步步看看您如何设置您的环境。
Unity 环境
微软 Visual Studio 是免费的,可以直接连接到 Unity,并附带一些工具,可以帮助您立即开始工作!这就像需要修理您的汽车,有人正好在你把手伸进引擎舱时给你正确的工具。
确保每个应用程序都能相互通信有一些步骤。让我们一起来了解一下,这样我们就可以确保我们在阅读本书以及本章的其余部分时处于同一页面上;我们将立即通过一些小的代码片段来工作。无需检查水温,让我们直接跳入!
- 通过谷歌搜索
Unity Hub并选择顶部链接来安装 Unity Hub。这将带您到官方 Unity 网页,您可以下载 Unity Hub。安装好 Hub 后,您需要安装一个 Unity 版本来使用。
如前一章所述,我们将推荐使用最新的 LTS 版本进行生产。在 Unity Hub 中,当您安装 Unity 版本且未安装 Visual Studio 时,将有一个选项为您预先配置安装。在下图 3.1 中,您可以看到我们已安装了 Visual Studio,但如果您没有安装,旁边将有一个复选框供您选择。然后它会为您安装好应用程序,准备使用!

图 3.1:Unity Hub 安装 Unity 模式
- 如果你没有安装 Visual Studio,那么这将为你配置 Unity 和 Visual Studio。如果你已经安装了它,你需要检查以确保程序之间的连接已经准备好协同工作。让我们将 Unity 连接到它并开始学习。
首先,关闭 Visual Studio 并打开我们在上一章中创建的 Unity 项目。如果你没有创建,现在是一个好时机。
导航到这些相应的菜单以将 Visual Studio 连接到 Unity:
Mac: Unity(屏幕左上角)-> 首选项 -> 外部工具选项卡
PC: 编辑 -> 首选项 -> 外部工具选项卡
在外部脚本工具下拉菜单中,选择 Visual Studio。选择后,转到项目窗口中的 Assets 文件夹,在灰色空白区域右键单击。根据 图 3.2,你可以通过选择:创建 -> C# 脚本 来创建一个脚本。将其命名为 ScriptingLesson 并双击它以打开。这应该会打开带有从 Unity 来的适当钩子的 Visual Studio。这意味着 Visual Studio 将读取项目文件,Unity 将根据任何更改保持其更新。

图 3.2:在编辑器中创建 C# 脚本
现在我们可以开始编写脚本了!在接下来的几节中,我们将学习基础知识。这些知识将伴随你,并成为你未来参与的所有项目的一部分。这是一个很好的地方放便利贴,以便于回来查阅。
在我们走得太远之前,我们想在这里强调一点。在这个章节和其他章节中,代码可能会变得杂乱无章。你的脚本可能因为单行差异或遗漏的分号而无法工作。这种情况非常常见。无需担心;我们将在项目中为所有脚本提供安全设置,以便你可以从中恢复。然而,需要注意的是,我们对代码行进行了大量的注释,以提高可读性。这可能会导致章节中陈述的行与你在代码中看到的不同。我们将尽力在书中保持准确,但有时单行注释可能会导致这种情况。请对我们保持灵活,因为注释可以在理解整个脚本的内部工作原理方面起到关键作用,比单行数字的准确性更重要。
基础知识
在安装并连接到 Unity 编辑器后,我们应该回顾一下基础知识。在本节中,我们将讨论数据类型、变量、逻辑或代码流程、方法、类和 MonoBehaviour。本章的这一部分包含了很多知识,但它的目的是为了查阅。如果你有便利贴,把它放在这一章里以便于查阅。当你打开文件时,会有自动填充的 C# 代码,这部分我们暂时不需要。现在,删除多余的部分,使其看起来像这样:
using UnityEngine;
public class ScriptingLesson : MonoBehaviour
{
// Data and Variables
// Logic and Flow
// Methods
}
这里的代码正在执行两个主要任务。第一行导入UnityEngine库,这样我们就可以使用UnityEngine命名空间中的类型和方法来为我们的游戏服务。这被称为“使用指令”。在UnityEngine命名空间内部,我们可以访问我们所有的游戏类型,例如 GameObject。由于我们将在这个编辑器中工作以操作这些 GameObject,因此我们应该在我们的类中使用这个命名空间。
下一个部分是一个名为ScriptingLesson的类,它继承自MonoBehaviour。继承是面向对象编程的一部分。这个类需要继承自MonoBehaviour,因为它直接影响到游戏中的对象。在下面的编程逻辑部分,我们将解释我们如何利用从MonoBehaviour继承。
// 表示注释。该行上的任何内容都不会被 IDE 编译。你可以使用这个功能来帮助你编写伪代码,或者通过在代码中添加一些定义性词汇来帮助其他可能与你代码一起工作的程序员。我们使用它是为了组织目的。
在你对脚本进行更改后,通过按Cmd + s或Ctrl + s来保存它。
如果你然后回到 Unity,你会看到 Unity 会编译脚本。每次我们对脚本进行重大更改时,我们都会回到 Unity 并检查我们的进度。有时 Unity 编辑器可能不喜欢我们正在处理的代码,但 Visual Studio 不会收到这些编辑器警告或错误。
在场景中,添加一个空的GameObject。将其命名为Scripting Lesson,然后选择它。在检查器中,点击添加组件并在搜索栏中输入scriptinglesson。左键单击脚本将其添加到空 GameObject。还有另一种添加组件的方法。如果你已经选择了脚本课程,你也可以点击并拖动脚本到检查器中,以将其添加到 GameObject 的组件部分。两种方法都很常见。小型项目可能比大型项目拖动得更多。当有多个脚本并且你知道你想要添加的确切内容时,你可以使用添加组件按钮并输入脚本名称来添加它。现在,当我们进行更改时,你将在这个 GameObject 上看到它们。
在我们讨论任何数据类型之前,我们应该有一个关于变量的简短讨论。
变量
就像在代数中一样,变量是某种内容的命名容器。C#是一种强类型编程语言。这意味着在声明时,每个变量都需要与其自己的数据类型相关联。有一些关于如何命名变量以及在某些情况下使用哪种数据类型的指南。我们将在每个部分中详细介绍这一点。命名约定是区分大小写的,每种命名类型都有自己的规则集需要遵循。
数据类型
在 C# 中,有 10 种 Unity 数据类型被使用,然而,在 Unity 中,我们主要需要了解 4 种。它们是 bool、int、float 和 string。我们将在我们创建的 ScriptingLesson.cs 文件中创建这些数据类型。
Bool
这代表布尔数据类型,它设计用于 true 或 false 变量。这些值也由 1(True)或 0(False)表示。
例如,这可以在你的角色进入他们不应该触发任何事件的地方时使用,比如一个 SPIKE TRAP!
在第 5 行添加:
public bool isActive;
这一行有四个部分,它们都有特定的用途:
-
public允许 Unity 访问我们在编辑器中创建的项目。 -
bool是我们创建的项目数据类型。 -
isActive是我们创建的bool数据的名称。它的默认值将是false。 -
在这里使用分号(
;)是为了表示指令的结束。
如果你保存并回到 Unity 编辑器,你现在会看到检查器中有一个名为 Is Active 的复选框。它应该看起来像这样:

图 3.3:“Is Active” 复选框可见
Int
整数,或 int,是一个完整的数字,例如 1、100、200、-234571 或 0。它不能有小数位。例如,如果你需要一个离散值来计数上升或下降,这会被使用。在游戏会话中收集了多少点是一个很好的使用 int 的地方。
在第 6 行添加:
public int myInt;
这与 bool 非常相似。我们声明 myInt 是一个公开可访问的整数数据类型变量。当你保存并回到 Unity 编辑器时,你现在会注意到一个名为 myInt 的变量右侧有一个文本输入框。由于它是一个整数,你不能在那里输入小数点(.)来创建一个十进制数,因为它是一个 int,只允许整数。
浮点数
你可能会问,我如何在数字中获取小数位?你寻求的答案是万能的 float!使用 float,你可以得到小数位,如 1.3 或 100.454。
float 有一个小独特的因素。当你编写脚本时,你必须在值后加上一个小 f 来帮助编译器知道该值是一个 float。C# 假设任何没有 f 结尾的数字,如 3.14,都是 double 类型。我们不会在我们的脚本中使用 double,所以我们需要记住在 float 后面加上那个小 f。
在第 7 行添加:
public float myFloat = 3.14;
当你尝试输入这个时,数字有问题,在 3.14 下面出现了一条红色横线,对吧?如果你将鼠标悬停在红色下划线区域,你会得到一个错误。它可能看起来像 图 3.4:

图 3.4:CS0664 错误显示
Visual Studio 正在试图告诉你你输入的数字将被视为 double,所以让我们做一点改变以帮助 IDE。
将第 7 行更改为:
public float myFloat = 3.14f;
好了。现在我们声明并初始化了一个浮点数。我们将其声明为 myFloat 并将其值初始化为 3.14。默认的浮点数是 0,但当你告诉它在声明时分配一个值时,IDE 会用你设置的值覆盖那个默认的 0。当你进入 Unity 并查看检查器时,现在你会看到值从 3.14 开始。
字符串
我们一直在这个时间都在处理数字。现在轮到字母发光了。字符串持有字符的值。这个例子可以是角色的显示名称。
在第 8 行添加:
public string mystring = "Myvari";
这看起来也差不多,但现在你可以添加字母!这里有趣的一点是,这些公共值的输入看起来都一样,所以我们需要确保我们的变量名是唯一的。
GameObject
这是一个有趣的数据类型,因为它对 Unity 来说是独特的。它的引用是从场景或预制件中放置在其内的 GameObject。这非常强大,因为放置在这里的项目有组件,我们可以从脚本中访问它们来执行许多事情。
在第 8 行添加:
public GameObject myGameObject;
保存你的代码并返回到编辑器。这次,你会注意到,它想要的不是一个输入字段,而是一个要放置在这里的 GameObject。在我们的场景中有一个方向光。让我们从层次结构中将那个灯光拖动到那个位置。你现在有一个场景 GameObject 的引用!让我们继续一点逻辑和流程,看看我们可以用我们的初始变量做什么。
编程逻辑
我们创建了一些充满如此奇妙数据的变量。唯一的问题是,我们并没有对它们做任何事情。为什么我们不开始在我们的小脚本中添加一些逻辑,以开始了解为什么我们最初需要编程呢?为此,我们将进入 if 语句 和 while 循环。
在我们进行一些运行时工作之前,我们需要将 MonoBehaviour 添加到我们的类中。我们即将采取的行动术语是继承。我们将从 MonoBehaviour 派生我们的类,这将通过继承它们来给我们访问其类的方法!这样做非常简单。
记住第 3 行:
public class ScriptingLesson : MonoBehaviour
我们通过在类名后添加 : MonoBehaviour 正确地进行了继承,现在我们可以访问 MonoBehaviour 类中的任何方法。我们可以从 MonoBehaviour 使用相当多的方法。现在,我们将使用从 MonoBehaviour 继承的 Start() 和 Update() 方法。
如果语句
我们现在将设置一些简单的代码,根据我们定义的 isActive bool 的状态来关闭和打开 GameObject。为此,我们需要在 update 方法中检查它,这是 MonoBehaviour 的一部分。
从第 13 行开始,我们做了以下更改:
private void Start()
{
isActive = true;
}
private void Update()
{
if (myGameObject != null)
{
if (isActive)
{
myGameObject.SetActive(isActive);
}
else
{
myGameObject.SetActive(isActive);
}
}
}
在 MonoBehaviour 的 Start 方法中,我们将 isActive 设置为 true。这是在这里添加的,以便将 Boolean 设置为在编辑器中设置的任何内容都应考虑。
之后,有一个update方法。MonoBehaviour中的update方法会在每一帧检查花括号内的整个代码。最初,我们通过将其与一个null进行比较来检查我们定义的 GameObject。这是一个有效性检查。Null是一个特殊的关键字,表示类型或数据的缺失。如果你不执行这些检查,你的编辑器将无法播放,因为将会有一个空指针异常。一个例子是,如果你在检查器中有一个未分配的公共 GameObject,这将抛出一个空指针异常,因为 GameObject 是空的!
在有效性检查中,我们有一个 if/else 语句。它目前表示如果isActive变量为 true,则将myGameObject设置为活动状态。对于除了 true 之外的所有情况,将myGameObject设置为非活动状态。
如果你保存并按下播放,你将能够选择脚本课程 GameObject,然后取消选中isActive布尔复选框。这将关闭灯光。由于这是每一帧都在检查,你可以一直这样做,直到你满意为止。
在我们继续到while循环之前,我们想要对我们的上面的代码块进行一些重构。这也是学习这个的好时机。我们通过这个if块学习了语法,但我们可以做得更好!我们每帧都在运行这个检查,所以这里的if块是不需要的,因为我们有一个Boolean可以与之比较。你可以用以下代码重构这个代码,节省计算时间:
private void Update()
{
if (myGameObject != null)
{
myGameObject.SetActive(isActive);
}
}
这样读取的方式是,每一帧,如果myGameObject不为空,则根据Boolean的设置将其活动状态设置为 true 或 false。我们不需要询问它是否为 true 或 false,因为数据类型只有两种状态!这真是太棒了。
让我们继续到while循环,看看如何循环代码。
while循环
if语句是一个简单的分支模式,用于检查 true 或 false 以执行某些操作。while循环将不断运行代码,直到语句为 true 或 false。这可能会引起问题——正如你可以想象的那样,一些任务可能会无限期地进行。这被称为无限循环,可能会无限期地挂起你的应用程序,或者直到它被强制关闭。大多数情况下,我们能够快速捕获无限循环,并且它们不会引起太大的麻烦。我们仍然应该在创建while循环时注意我们的标准。
在第 32 行,将这些行添加到update方法中:
while (MyInt > 0)
{
Debug.Log($"MyInt should be less than zero. It's currently at: {MyInt}");
MyInt--;
}
在这个while循环中,我们正在做几件新的事情。我们正在执行一个调试日志、字符串插值和一个递减器。让我们逐一来看。
调试日志
Debug.Log允许我们传入一个字符串,它将在 Unity 的控制台内输出。如果有奇怪的事情发生,并且你想要在控制台获取一些运行时信息,这非常有帮助。
字符串插值
在日志内部,我们执行一个称为字符串插值的操作。这是一种非常方便的方法,可以将变量添加到字符串中。它从$符号开始,后跟双引号。在这个双引号内部是一个你想要写出的字符串。它是一个包含空格的文本字符串。有趣的是,字符串内部有花括号{}!如果你在花括号内放置变量名,你将得到传递到字符串中的数据。在上面的while循环中,你可以看到我们在Debug行中执行此操作。
递减器
下一行是一个递减器。这是编写此行的有效方法:
MyInt = Myint – 1;
如果你保存然后运行它,控制台将没有任何内容。这是因为我们没有设置MyInt的值,所以它默认为 0。由于MyInt不是大于 0(它是 0),while循环将不会运行。让我们进行以下更改:
在第 16 行,添加以下内容:
MyInt = 10;
保存并运行游戏。现在如果你查看控制台,它将快速将MyInt递减到 0 并打印出显示其当前值的行。如果你查看检查器,你也会看到MyInt显示为 0。
现在我们对编程逻辑有了些了解,让我们添加一些功能。
for循环
与while循环一样,for循环是对一组固定项的迭代。如果有一个迭代需要运行多少次的预期,那么for循环是最常用的。我们将执行一个简单的for循环来展示其语法,并在下面进行说明:
首先,让我们注释掉第 35 行,因为我们不需要while循环调试日志在我们的for循环调试行中造成混乱。
然后,在第 39 行,添加以下代码块:
for (int i = 0; i < 10; i++)
{
Debug.Log($"For Loop number: {i}");
}
for循环通常更常见,但让我们简要讨论一下为什么你可能想要使用其中一个。
选择for循环和while循环
for循环和while循环在功能上相似。它们被设计为遍历一组固定项。关于这些规则并没有一成不变的规定。技术上它们可以互换,但在可读性方面有一些细微差别。for循环看起来像是有固定数量的迭代。这个值不需要for循环知道,但一个例子是一组 GameObject。如果你需要遍历所有这些并对其执行逻辑,你不需要编写要迭代的项数,因为分组有一个计数。你可以遍历这个计数。我们将在第六章,交互和机制中看到这个例子。
这与while循环的区别在于,while循环的读法如下:在条件设置为真之前执行某些操作。无论需要迭代多少项,它都会一直执行,直到满足另一个条件。while循环在制作无限循环方面存在固有的问题。如果你不完全理解你正在循环的内容,或者意外地使用了错误的符号(<而不是>)作为你的条件,你可能会遇到while循环部分中解释的无限循环。while循环不像for循环那样常用,但在编程中也有其合适的位置。
方法
如果逻辑是编程的黄油,那么这就是面包。方法的目的是以简洁的方式执行操作。一个方法非常简单的例子是一个基本的计算器函数,称为Add。为了执行这个函数,我们将做三件事:创建一些公共变量来进行加法运算,有一种方式可以触发方法,以及方法本身。我们决定使用输入系统来处理所有输入。为了使它工作,这里还需要添加一些新的概念。之前,我们要求你将代码插入到特定的行中。现在,我们只要求你将代码插入到特定的部分。
在顶部,我们需要让程序知道我们想要使用输入系统。为此,我们将添加一个using语句:
using UnityEngine;
using UnityEngine.InputSystem;
在类的变量部分,添加以下这些行:
public int addA;
public int addB;
public int totalAdd;
private InputAction testInput = new InputAction("test", binding: "<Keyboard>/b");
我们将int变量设置为公共的,这样我们就可以修改它们,并在运行时运行方法时看到它们是可以被修改的。输入系统是私有的,因为它不需要受到任何其他脚本的干扰。这是编写代码时需要考虑的良好实践。如果一个变量不会被其他脚本修改,那么就将其保持为私有变量。虽然它可能不会对小型项目的工作环境产生不利影响,但在项目更加完善时可能会出现冲突。我们希望从一开始就保持代码的整洁。
InputSystem需要输入具有开启的监听器,并在不使用时禁用。我们将创建两个原生于MonoBehaviour的方法;OnEnable和OnDisable。
private void OnEnable()
{
testInput.performed += OnTestInput;
testInput.Enable();
}
private void OnDisable()
{
testInput.performed -= OnTestInput;
testInput.Disable();
}
这些方法在运行时自动触发。OnEnable在初始化后直接位于Awake之后。不要担心这有点多,我们将在本书的不同章节中多次回顾它们,从不同的角度进行讲解。
目前,这些方法在这里的原因是当执行testInput时添加OnTestInput方法。我们在变量部分将字母B绑定到我们的输入,现在我们正在添加一个当按下时执行的方法。
在update方法下方和外部,让我们添加我们的添加方法。
private int IntAdd(int a, int b)
{
totalAdd = a + b;
return totalAdd;
}
这是一个私有方法,这意味着在这个类之外,我们无法访问这个方法。这个方法将返回一个int,其名称为intAdd。在名称之后括号内的参数是方法的参数。我们有两个整数:a和b。我们需要定义它们的数据类型和名称。当方法运行时,我们的方法会创建两个具有当时值的整数,并将它们分配给变量a和b。我们将totalAdd设置为它,这样我们就可以在检查器和控制台中显示值的变化,以便进一步调试。
为了将这些内容整合在一起,我们需要创建OnTestInput方法。这个方法包含了一些新术语,但在这个例子中,我们将把它们提供给你,以便开始对简单按钮点击进行测试。稍后,在本书的机械部分,我们需要在输入中包含更多的逻辑。在早期设置好这个系统,可以快速迭代和扩展新的输入方案,例如控制器。
在intAdd方法下方创建一个新的方法:
private void OnTestInput(InputAction.CallbackContext actionContext)
{
// If the action was performed (pressed) this frame
if (actionContext.performed)
{
Debug.Log(IntAdd(addA, addB));
}
}
这里的魔法在于,这个方法被放置在我们在脚本中启用的testInput的performed上。这个脚本会调用分配给该输入的动作。目前,我们只运行简单的逻辑,以便让调试日志打印出如果actionContext被执行。
在我们的情况下,当方法被调用时,这将是真的。如果我们需要其他逻辑,例如如果技能的冷却时间还没有完成,我们可以在该方法中告诉用户他们不能在这个方法中执行那个技能。这是一个非常强大的系统,可以构建得非常健壮。
在 Unity 中,点击层次结构中的脚本,然后在检查器中为addA和addB变量输入一些值。开始游戏并按b键。你应该会看到totalAdd的变化,以及控制台打印出Debug行中的数字。
摘要
这可能是你第一次阅读关于编程的任何内容。通过这些小例子,你将获得坚实的基础。花时间完全理解本章中我们讨论的内容,因为它将是连接其他章节的粘合剂。在后面的编程过程中,我们将使用所有这些特性,以及添加新的库和类的不同实现。这些都是编程的基础;我们将在整本书中彻底地构建在这些基础之上。如果你迷失方向,可以参考 GitHub,在那里你可以找到这些脚本的完整形式,如果你发现某些东西工作不正常,可以参考它们。
这是我们将要在接下来的五章中大力构建的基础的终结。第二部分:构建与设计将补充你迄今为止所学的所有内容,回答更多关于构建原型设计的问题,并展示 Unity 如何帮助你尽可能轻松地创建游戏。让我们继续构建角色,并将一些编程技能结合起来,让 Myvari 能够通过输入移动。
第四章:角色
在第二章,设计和原型中,我们讨论了这本书将采用垂直切片方法来构建我们的模型。作为一个垂直切片,这个项目是对游戏的简化。它类似于一个演示,但包含了游戏将包含的所有主要机制,只是形式更为简单。它的目的是向投资者展示一个可能成为完整游戏体验的强大例子。我们将展示角色的行为来吸引玩家,然后通过环境谜题驱动的机制引入故事的小部分,同时了解主要角色的过去。
我们将从与主要角色相关的概念开始;然后我们将建模角色,并在通过他们的机制和动作进行工作时,根据我们的喜好进行修改。我们将为他们制作一个布线,以便他们可以被动画化。之后,我们将它们放入 Unity 中,测试在引擎中移动它们的感觉。这一章将包含大量信息,我们将讨论许多不同的制作概念来创建我们的角色,并确保它们能够正确移动:
-
设计和概念
-
布线
-
角色控制器
-
编写角色的动作脚本
让我们从 Myvari,我们小故事的主角的概念阶段开始。
设计和概念
要创造一个角色,可以有多个维度。我们想确保这个垂直切片中的英雄角色 Myvari 尽可能丰满。为此,最好的工具之一是询问“为什么”关于角色的各个方面。
询问为什么
我们正在构建一个基于冒险的解谜游戏。首先想到的问题是:为什么这个角色在这个冒险中?在这种情况下,我们用“她正在寻求找到关于她种族过去的答案,这些答案不在被讲述的故事或她读过的书中。”来回答这个问题。现在我们已经定义了一些事情,但还有更多问题。
为了给出从这个初步答案中产生的想法,看看这个列表:
-
她是什么种族的,这为什么对故事很重要?
-
为什么是女性?
-
为什么她的历史被隐藏了?
-
她的服装是什么样的?
-
这个种族看起来是什么样子?
-
她是类人形吗?
如您所见,这可以持续一段时间,而且应该如此。所有这些答案都应该带来更多问题。将它们简化到最低可能的组成部分。这可能看起来很繁琐,但当你完成时,你就会知道这个角色在面对挑战时的行为、面部表情、行为、内部笑话、家庭背景,以及所有介于其中的事情。
概念时间!
现在我们对 Myvari 和她是谁有了强烈的认识,我们可以绘制概念艺术。我们首先从一些比例工作和草图工作开始,以找到她基本的外观。
在左边的 图 4.1 中,我们还绘制了一个行为草图。这让我们对她在 空闲休息 动画中的可能行为有一个视觉概念。空闲休息是一种当你的角色静止一段时间时发生的动画。我们觉得她的角色会勤奋好学,所以她会拿出书并立即开始学习。

图 4.1:初始 Myvari 草图
在我们绘制出她可能看起来像的草图,包括给她一个性格感之后,我们需要对颜色有一个感觉,以最终确定设计。图 4.2 中显示的配色方案是在回答了所有之前的问题后选择的。
Myvari 的整体色彩主题给人一种皇室、好奇和安全感。这些感觉通过华丽的服装和金色线条来体现皇室地位。蓝色唤起一种安全感——一种心理和生理效应,因为看到这种颜色会降低我们的血压。使用蓝色会让玩家稍微更多地进入她的角色,对她中性色调服装中的这种明亮的蓝色感到好奇。
她最独特的配饰是她的项链,它具有机械功能。它将是她必须与之互动的谜题的关键。因此,项链的颜色与 Myvari 的其他颜色形成鲜明对比。这种颜色在环境中也将是独特的,它利用了一个称为 用户引导 的概念,我们将在 第五章,环境,第六章,交互和机制,第七章,刚体和物理交互 和 第十二章,最终润色 中讨论。我们需要在环境、机制以及整个垂直切片的打磨中持续使用这种颜色。

图 4.2:Myvari 的配色方案
一旦我们对角色的性格和颜色有了强烈的想法,我们就需要进入概念设计的 3D 版本。这更多的是定义一个角色,而不是创造。以下图像使用 雕塑 工具描绘了角色的面部特征。我们的团队在 Pixologic 的 ZBrush 的使用方面有很强的背景,这使我们能够创建 图 4.3 中看到的雕塑。3DCoat 和 Blender 也提供了雕塑工具。
在我们对雕塑进行了足够的迭代工作后,我们将使用这个作为起始的高分辨率模型。
现在我们已经定义了我们的主要高分辨率雕塑,我们可以继续进行从 ZBrush 中获取低分辨率模型的工作。

图 4.3:Myvari 的高分辨率头部雕塑
低分辨率模型将成为游戏中所有事物的比例偏差,如图 4.4 中所示。当你创建建筑、植物、动物或角色时,这个模型将作为它们的通用 比例。然而,有多种处理比例的方法,但我知道这个角色是游戏中的主要生物;所有物品都将作为环境或道具,它们都将基于她的尺寸作为基础比例。

图 4.4:Myvari 的低分辨率头部雕刻
其他一些进行缩放的方法是通过按比例构建。Unity 的单位是厘米。如果你按米来构建,然后从你的数字内容创作(DCC)以厘米为单位导出,所有内容都将遵循单一的单位比例。如果你正在构建一个由世界各地的许多团队协作的大型游戏,这是一个很好的构建方式。你也可以围绕环境本身来构建游戏。一个例子可能是使用方块来创建环境的俯视游戏,这样所有东西都能完美拼接。方块可能是屏幕的 1/10。从这个信息中,你会在预期的分辨率下拍摄一张截图,然后在图片上绘制角色概念以适应比例。看看你的项目,看看你可以构建的单一比例点。这最终会节省你未来的时间。
你可能只是构建你能构建的任何东西,然后在导入到游戏中后对其进行缩放。然而,这样做的结果是,机制可能不会正常工作。想想像《古墓丽影》或《刺客信条》这样的游戏,其中主要角色必须爬上墙壁。这要求角色具有非常特定的身高和大量的训练,以确保他们在动画的正确时间和地点。
布线
在完成整个概念阶段的工作后,我们需要在角色中添加一些骨骼,以便我们可以对她进行动画处理。我们将使用 Autodesk 的 Maya 2022 来为我们的角色布线。我们将讨论的原则不是技术细节。根据你的 DCC 工具,你可能会遇到略微不同的术语,但是以下术语通常适用于任何用于游戏开发的 DCC 的主要工具。
以动画为先的思考
在开始布线任务时,最有效的工作方式是与将负责这些动画的艺术家进行详细的对话。即使你自己做动画,成功的布线应该是确保动画师不需要解释每个控制的作用。可能会有一些技术属性,但总体来说,如果一个控制不需要某些东西,它应该被锁定并隐藏。
当动画师移动控制手部的控制时,他们可能期望所有手指控制都在那个控制上。这是直观的,但不应该假设动画师需要这个。他们可能希望所有单独的控制都放在他们自己的控制上。
变形
这是网格能够以预定方式弯曲的能力,例如肘部或膝盖。由于你知道它们会如何弯曲,你可以规划你的网格,使其在模型中通过适当的边流实现这种变形。边流是一种艺术形式,它确保在变形时有足够的几何形状来保持形状。一个边流的例子可以在图 4.5中看到。花时间研究其他 AAA 模型示例,了解每个身体结构可能如何弯曲。

图 4.5:人类边流示例
面部变形是最具体的变形。如果你在开始绑定时计划进行任何面部变形,寻找一些解释如何为这种变形设置角色的视频。面部表情复杂且难以正确完成。
在图 4.6中可以看到边流和分离的小示例。

图 4.6:面部边流示例
正确完成面部变形将使你的角色产生令人信服的表情。这可以通过增强情感沉浸感来增强体验。如果你计划在角色有特写镜头时,花时间研究面部变形是个好主意。
层次结构
层次结构和父子关系是绑定知识的组成部分。当 Unity 导入骨骼网格时,你的 DCC 可能已经设置了变换。这有时可能是一个分组或层次结构变化,它有自己的变换。Unity 将这些项目视为 GameObject,并将它们放置在骨骼层次结构中的相同位置。每个应用程序可能略有不同。例如,在 Maya 中:如果你的层次结构有一个组节点作为你的绑定的父节点,Unity 会认为这是一个它自己的变换并将其导入。这可能不会在开始时引起问题,因为 GameObject 上没有逻辑,但始终最好尽可能干净。如果你使用 Maya,我们建议你不在你将要绑定的骨骼上使用任何父节点。
这引出了一个有趣的工作主题:控制绑定。我们使用过的最可定制的绑定是绑定绑定,它绑定到角色的所有顶点上。然后,它直接由一个副本绑定驱动,我们称之为控制绑定。这允许绑定绑定只关注单个实体的输入。这很重要,因为有时你可能希望有多个变形工具在绑定周围移动。你可能需要一个单独的挤压控制和扭曲控制。所有这些逻辑都在控制绑定上,而不必担心破坏角色的绑定。
骨骼或关节
在绑定中,骨骼和关节是可互换使用的术语。绑定骨骼在其自己的层次结构中仅由骨骼或关节组成,从根节点开始,经过脊柱、手臂、腿部、颈部和头部。这些骨骼可以在下面的图 4.7中看到。

图 4.7:DCC 内部角色关节的示例
在你布置好骨骼之后,你需要通过规划驱动骨骼结构的约束系统来规划下一级。如果你在顶部使用控制骨架,这些约束将驱动控制骨架的骨骼。
前向运动学/反向运动学
前向运动学(FK)和反向运动学(IK)是手臂和腿部动画的两种主要形式。FK 是一种技术,其中动画师将手动单独旋转每个关节。如果你在动画化一个手臂,你会从肩膀开始,然后是肘部,然后是手腕,等等。它被称为“前向”,因为你是在沿着动画层次结构向下前进。相反,IK 是你将动画化手部,肩膀和肘部会根据旋转平面的引导而跟随。关于使用哪一个存在争议,然而,它们都是工具。如果你使用其中一个并且它使你的工作变得更简单,那么就使用那种风格。在角色骨架上看到 FK/IK 切换是非常常见的,因为它们在特定的动画工作流程中都有其位置。
此外,关于 IK 的主要担忧是你可以保持手部在相同的位置或者到它最后放置的空间。想象一下站起来,将一只手举到空中,然后上下移动臀部,同时保持手部在空中相同的位置。如果只使用 FK 设置,这将是一个非常繁琐的动画过程,因为你必须分别对所有臀部的动作进行关键帧设置。使用 IK,你将能够将手腕设置在它应该在的位置,然后只需动画化臀部。IK 会为你处理肩膀和肘部。然而,对于重力作用于手部并且主要只是随着运动动量画出一个弧线的情况,FK 更适合。
如前所述,这些是可能产生相同结果的工具。随着你使用工具的经验积累,它将为你提供关于你的动画风格的洞察。
约束
限制是一个简单的动作。使用视觉对象,我们希望动画师能够立即理解控制的目的。一个简洁的例子就是一个NURBS(非均匀有理 B 样条——空间中的一个点,可以创建视觉元素)曲线,它会指向一个装上骨架的手的每一个手指,以帮助制作拳头。下方的图 4.8展示了我们如何使用 Myvari 的骨架来完成这个动作。

图 4.8:Myvari 的手部控制
这些被称为控制,仅仅是因为它们允许动画师控制角色的某些方面。阅读这篇文章可能会让人联想到我们在第一章中介绍过的另一个术语:父子关系。确实,约束和父子关系之间存在相似性,然而,我们可以具体指定我们想要用约束来约束的内容。在 Maya 中,这被分为平移、旋转和缩放约束。你还可以约束每个这些组件的单独部分,例如“仅旋转x”。这允许绑定师稍微限制动画师。上面的头部控制示例可能只需要旋转约束。这使得盒子的平移不会影响骨骼。使用父子关系,你无法将这些分开。父对象会影响子对象的全部变换。
变形器
变形工具将因每个 DCC 而异。变形器的主要功能是以某种方式控制顶级层次结构。一个例子可能是一个扭曲变形器,它可以允许控制有一个很好的扭曲,使动画师更容易构建扭曲动画。
一些高级动画使用控制骨骼的另一个网格上的变形器,这有时被称为带子绑定,如图 4.9 所示。这张图片显示了左侧的带子和控制项链下关节的变形器。右侧显示了动画师通过隐藏下方的带子控制所看到的内容。

图 4.9:Myvari 项链的带子绑定的示例
控制
动画师在 3D 世界中有一个独特的工作,那就是实时处理他们需要移动的物品的表示。无论是围绕手或头的盒子,还是每个形状都给动画师提供了控制。每个角色都会有满足其需求的独特控制。Myvari 将会有双足的标准控制和额外的控制,用于她的服装和饰品。
下图 4.10 显示了角色的全身控制。

图 4.10:Myvari 的全身控制
基于物理的动画
一些动画可以通过模拟来完成。应该有一个骨头连接到网格上,但 DCC 将执行稍微受限的物理运动。这些对于链条、项链以及基本上任何有弹性的或悬垂的东西都非常适合工作。这些事情用手动画是非常困难的,所以最好让应用程序来处理。在某些情况下,游戏引擎可以处理所有的基于物理的动画,不需要动画师进行关键帧设置。这意味着物理动画将独立于动画文件本身,这将允许动画的平滑混合。
人类逆向运动学(HIK)系统
Autodesk 创建了一个双足装置系统,以便轻松集成多个软件。这主要用于动作捕捉工作,其中动画是通过不同的技术创建的。动作捕捉是通过服装、面部捕捉设备以及专业手套完成的。
人逆运动学(HIK)装置的主要目的是收集双足角色的头部、脊柱、手臂和腿部的动画数据。有一个高级版本允许手指以及手臂和腿部的更多功能,例如扭曲。为了收集更多关于 HIK 骨骼的信息,Autodesk 已经发布了关于如何最佳使用它的文档。在我们的演示中,我们不会使用 HIK 系统。使用 Myvari,我们将进行所有手动画制作,而不进行动作捕捉工作。了解这一点后,我们决定只保留自定义装置和控制系统。
动画
我们现在已经设计、建模并装备了一个带有控制器的角色。现在我们可以使用我们的动画技能在我们的 DCC 中给角色赋予生命。当你决定制作哪些动画时,一定要仔细考虑角色的性格。由于我们花了很多时间询问关于角色动机和欲望的难题,我们应该用符合这种性格的正确动作来尊重这一点。
处理动画的一种强有力的方式与处理任何艺术形式的方式相同。首先,我们将通过仅使用关键姿势来获得正确的时机,进入一个阻塞性阶段。当你这样做的时候,尽量使关键姿势尽可能强。每个关键帧都应该有个性。如果你看关键帧,无法感受到角色的感觉,那么它就不会被认为是角色的“关键”。在你放置了一些关键帧并将它们移动以获得时机感之后,这就是你添加中间帧的时候了。
这些是在关键姿势之间的关键帧,这些关键帧将有助于展现每个关键姿势内的动作。
一旦达到这个阶段,将动画添加到游戏引擎中可能是个好主意,以获得与角色控制器实际动作的感觉,看看你在 DCC 中看到的是否能转化为游戏中的动作。在这个阶段做这个是明智的,因为你在项目接近尾声时将有大量时间学习如何移动角色后,对所有的动画进行润色。
角色控制器
现在我们已经完成了角色设计、模型和绑定设置,我们需要设置一个控制器来使它们对输入做出反应。角色控制器有两种一般方法。Unity 提供的内置角色控制器将允许您让角色四处走动、上楼梯,并轻松地构建进一步的交互功能,但它有其局限性。最大的局限性是它不作为物理对象使用。如果你需要你的角色被物理力推动,还有一个第二个选项可用。第二个选项是使用刚体和碰撞胶囊,以及使用这些作为对物理引擎限制的角色脚本。正如你现在可能已经预料到的,为了选择正确的选项,我们需要提出问题!以下是一些例子:
-
主要机制是什么?
-
我需要物理运算来移动吗?
-
我的角色将有多少其他限制?
经过一段时间,当你开始看到在 Unity 中实现你想要的玩法可能需要什么架构时,你可能学会在早期就提出这些问题。这只有在这些问题没有得到回答,并且稍微偏离目标之后才会发生。不要因此气馁。这是最好的学习方法:快速失败和经常失败。可用的选项并不总是显而易见的。
这的一个例子是问自己主要机制是什么。看看你的游戏,你可能试图推动游戏中的武器制作,但在制作过程中战斗机制更有趣。在弄清楚这一点后,你可能削减大部分制作工作,并更多地投入到打磨战斗机制中。这样做实际上是在角色控制器上比 UI 或交互式制作工作更强调。
在我们的游戏中,我们决定对角色移动采取简单的方法。我们只需要移动,所有其他交互将通过鼠标位置和相机操作来完成。考虑到这一点,我们将研究角色控制器的基础,以便在此基础上构建。
内置角色控制器
Unity 内置了一个可以添加到您角色上的角色控制器组件。这将为您的工作提供坚实的基础。它只是一个胶囊碰撞体,允许第一人称或第三人称游戏进行简单的移动。有趣的是,它不使用物理或刚体进行物理运算。Unity 文档将其最佳解释为“Doom 风格”控制器:移动非常快,当你松开摇杆时,它立即停止。这有时是可取的,但并不常见。一个可能需要这种功能的例子是当你制作一个需要极其精确控制的游戏时。Metroid 使用这个功能立即左右翻转角色。如果你在转向前必须减速到停止,游戏的感觉就不会像现在这样好。
最好的部分是,如果你只是想测试一些简单的东西,将它应用到角色上以使其移动既快又简单。如果你想添加跳跃、漂浮、游泳、飞行或任何与物理相关的动作,没有大量的工作,这个应用程序将无法工作。
我们将在这个教程中使用内置的角色控制器,因为 Myvari 只需要在地面上探索,不需要跳跃或滑动,而且她所有的交互都不需要任何特定的物理效果。
Rigidbody 角色控制器
这个组件的选项从编码的角度出发,但提供了内置角色控制器在很多情况下无法提供的灵活性。使用 Rigidbody 的初始理由是如果你想在游戏中使用不同的物理材料。如果你的游戏计划以多种方式利用物理,那么计划与 Rigidbody 和碰撞组件一起工作作为你在角色控制器上的物理选择会更好。
编写角色的动作脚本
当你在编写角色脚本时,进行尽可能多的与动作相关的设计讨论是个好主意,以便知道要构建什么。对于 Myvari,我们希望在环境中有一些与动作相关的具体设置,因为这款游戏是一个环境解谜游戏。我们应该让环境在她穿越时与她互动。以下是我们讨论过的列表:
-
空闲
-
走路:
-
在地面上
-
在水中
-
在边缘上
-
-
旋转
目前,我们还没有完全决定实施两个与动作相关的脚本。这些是跑步和跳跃。我们目前不打算实施这些动作的原因是我们不能确定我们是否真的需要它们。在我们目前通过关卡的过程中,走路的感觉很好,我们希望玩家也能关注环境。如果我们认为以后需要的话,我们会设置角色控制器以接受跑步动作。跳跃类似,但我们没有需要跳跃上升或跨越的机制。你只会实施这个来满足在环境中跳跃的需求。我们可能会在经过一些质量保证测试和玩家反馈后,发现这确实是必要的。如果这是一个强有力的案例,我们可以添加它。
Unity 中的初始设置
首先,为了使 Myvari 能够接受动作脚本,我们应该在 Unity 中设置它。我们应已将 Myvari 导入到 Unity 项目中。只是让你知道,进行操作的方式只是简单地将它拖放到你希望她所在的项目的文件夹中。如果你在“角色”文件夹中选择 SM_Myvari,检查器将显示模型上的导入设置,如图 图 4.11 所示。这里使用的默认设置对我们来说已经足够好了。

图 4.11:导入设置 模型选项卡
我们需要切换到 Rig 选项卡并设置我们的装置。如图 图 4.12 所示,我们有几个选项要讨论。我们想确保 动画类型 设置为 人类。我们还想从这个模型创建一个头像并对其进行配置。这将打开另一个窗口来设置人类结构中的骨骼。

图 4.12:导入设置 Rig 选项卡
此窗口将默认显示身体部分,尽管我们在 图 4.13 中展示了头部部分。最好检查身体的每一部分,因为头像系统会尽力将关节对齐到正确的位置,但有时可能不起作用。如果没有正确设置,只需选择正确的关节以正确填充插槽即可。

图 4.13:导入设置 Rig,配置头部部分
在设置控制器之前,我们应该讨论一下关于游戏如何玩的决定。我们有一个第三人称、肩上视角的玩法。这意味着我们必须给角色添加一个摄像头。因此,我们应该制作一个包含我们的角色和摄像头的 Prefab。为了设置这个,我们创建一个层次结构,它允许独立地移动摄像头,但保持摄像头和角色在一起。如图 图 4.14 所示,你可以看到我们如何设置 Prefab。

图 4.14:角色 prefab 层次结构
我们这样设置的原因是我们想要一个容器来同时容纳摄像头和角色。角色 GameObject 包含所有用于角色的脚本。网格将包含动画器和头像。摄像头装置将容纳摄像头以及保持摄像头在我们想要的位置所需的脚本。在本书后面的 第六章 中,当我们进入机制时,交互和机制,我们将详细讨论 Cinemachine,因为在游戏中的一些部分我们需要将摄像头放置在电影拍摄的位置。
在本章的剩余部分,我们将简要介绍如何设置角色移动的基本设置。在角色 GameObject 上,让我们设置组件以使她能够移动。如图 图 4.15 所示,我们将添加四个更多组件。这些是角色控制器、我们的移动脚本、一个 Rigidbody 组件和一个玩家输入系统。

图 4.15:角色 GameObject 组件
如前所述,我们将使用基础角色控制器。这些设置是主观的,我们还没有最终确定,但这是我们目前所拥有的。在此我们应该添加的一个注意事项是关于 中心属性。这是角色控制器认为角色中心的位置。它默认在地面,但您需要将其向上移动,使胶囊体更靠近中心并稍微离开地面。我们尝试将其放在骨盆附近,然后使用半径和高度来包围角色的整体身体。我们这样做是因为骨盆控制整体高度,因为人体结构的质量中心在肚脐处。
我们现在将跳过移动脚本。这里的 Rigidbody 是为了帮助未来的机械需求以及基于物理的工作。我们将在 第五章,环境 和 第六章,交互和机械 中介绍这一点。
PlayerInput 是一个 Unity 系统,它设置模块化输入,以便轻松添加不同的输入系统,而无需更改代码。首先,打开您的 包管理器,查看是否已安装 输入系统。它将是 Unity 注册表 的一部分。如果没有安装,请安装它!如果已安装,那么我们需要为我们自己创建一个输入系统。
这可以通过添加一个名为 输入动作 的新资产来实现,如 图 4.16 所示。

图 4.16:添加输入动作资产
在创建输入动作后,根据您的需求为其命名。我们将其命名为 Player Actions。我们将专门使用这个输入动作分组来处理任何所需的玩家动作。在未来的项目中,您可能需要除了角色之外的其他动作。在此阶段,您需要双击资产以打开 输入动作 窗口。在这里,我们将设计用于当前所需的 Myvari 的输入选项。
图 4.17 展示了我们目前所需的完整输入系统。随着垂直切片的持续开发,我们可能会添加更多输入。

图 4.17:输入动作
动作映射 是一组具有自己可以调用的动作的分组。属性是所选动作的细节和选项。对于这个案例,我们只需要 Myvari 的输入,因此我们创建了一个 Myvari 动作映射。请注意 动作映射 名称的大写,因为它一旦进入移动脚本编写阶段将被使用。
在动作中,绿色部分是动作本身,蓝色是绑定,浅红色是绑定部分。对于移动,我们只需要关注向量的组合。当你添加一个新的绑定时,如果你在动作的右侧按下加号(+)符号,你有两个选项。这些是绑定或2D 向量组合。当你点击2D 向量组合时,它会自动添加上、下、左和右的组合部分。我们目前将它们定义为键盘输入,以保持一定的输入系统。在设置动作时,有一个非常有趣且有用的工具,称为监听按钮。查看图 4.18,你可以看到它被按下并且正在监听输入。对我们来说,能够按下可能被按下的假设按钮,给我们一种即时的玩家反馈感。如果在这个时候将按键分配给动作感觉奇怪,那么在游戏过程中也不会感觉好多少。

图 4.18:监听输入
查看输入用于相机移动,我们使用Delta进行鼠标移动。我们的瞄准动作是为了当你按下右鼠标按钮进行精确操作时。这是选择动作类型为按钮并期望右鼠标按钮输入。最后,我们有一个交互按钮。这与瞄准相同,但设计为在特定时间按下E键。这些时间将在环境和交互和机制中定义。
我们现在为玩家输入的游戏设置了基础框架。即使我们编写了与这个输入系统协同工作的脚本,它也不会影响任何事情。因此,在我们开始编写脚本之前,我们需要为 Myvari 准备动画设置的基础。让我们看看我们需要哪些动画。目前,我们只需要空闲和行走动画进行过渡。在这里我们暂时不需要设置交互,因为我们目前还没有用到它。在第五章,环境中,我们将探讨交互的使用。
空闲
当玩家四处张望时,Myvari 可能需要暂时保持静止。大多数时候,空闲时不需要编写脚本,因为这应该是你的动画控制器中的标准状态。当你将角色添加到场景中时,你需要添加一个动画控制器组件。参见图 4.19以获取正确的配置。

图 4.19:动画控制器组件
控制器和角色将是空的。我们需要通过创建一个新的资产并转到创建>动画控制器来创建控制器。控制器是代码和视觉效果之间的接口,用于移动我们想要动画化的骨骼网格。
对于Idle,我们将创建一个默认状态并命名为Idle。您可以在图 4.20中看到这一点。在项目中的Characters > Animations文件夹内,有我们使用 Myvari 设置的动画。选择Idle状态,将此文件夹中的空闲动画拖放到检查器中的Motion参数上,如图图 4.21所示。

图 4.20:控制器状态机

图 4.21:空闲动画状态检查器
当您放置好空闲动画时,当您按下播放,角色将进入空闲模式并无限循环该动画!
我们还希望有一个行走动画。为此,在空白区域右键单击,选择创建状态,然后选择空状态。
将其命名为Walk。选择它并添加行走动画。之后,在Idle上右键单击并选择创建转换,然后左键单击Walk状态。
这将使状态从空闲转换为行走。从行走状态返回空闲状态时,请执行相同操作。这允许我们设置从空闲和行走状态转换的参数。现在,我们将在控制器中添加一个名为isWalking的参数,如图图 4.22所示。

图 4.22 控制器参数
参数部分位于控制器的右上角。我们想要创建一个布尔值并命名为isWalking。我们将在转换点使用此参数。如果您选择从Idle到Walk状态的转换,您将在检查器中看到从一种动画到另一种动画的转换。检查器的底部是条件。让我们添加一个条件并将其设置为isWalking is True。当isWalking为true时,Idle的动画状态将转换为Walk。然后您可以执行相反操作以返回Idle。
我们现在已设置好输入和动画,并准备好转换以监听角色逻辑。现在我们需要进入那里并使移动脚本生效。让我们深入代码!
代码入口点
我们在这里添加一小节,解释我们将如何介绍代码。在第三章,编程中,我们逐行介绍了代码的基础知识。我们计划在这里提供完整的脚本,并附上完整的注释,以便您在更熟悉代码的情况下阅读。在本章的其余部分,我们将介绍之前未介绍的部分,并作为工具进行解释。我们鼓励您构建自己的脚本,并使用我们介绍的工具构建自己的角色移动脚本。
我们将处理MyvariThirdPersonMovement.cs文件。这里有一些简单的工作,以及一个复杂的功能。当我们处理这些时,要知道不完全理解所讨论的内容是正常的。通过注意到难点并解决它们,你正在巩固这些知识,并了解如何在 Unity 中作为开发者工作。
RequireComponent
当你在类定义上方看到RequireComponent时,这意味着这个脚本附加到的 GameObject 需要有一些东西。在我们的情况下,我们希望在角色上MyvariThirdPersonMovement.cs,并需要确保它有一个角色控制器。非常有帮助的是,如果 Unity 看到你附加的 GameObject 没有所需的组件,它将直接为你将其附加到 GameObject 上!这不是很好吗?我们认为是的。
[RequireComponent(typeof(CharacterController))]
更新代码
我们将详细地处理这一部分,因为每一行都深入探讨了之前的信息,而且如果不展示上下文,很难解释单行。对于第一部分,我们想要确保如果角色是地面上的,并且他们的速度不是高于0,则将其设置为0。有时 GameObject 会在y方向上以小的增量移动。这并不常见,然而有时在 3D 应用程序中,旋转和移动会导致值和速度的舍入,这可能导致不应该增加的增量。可以通过使用这些几行代码来防止这种情况。
if (controller.isGrounded && playerVelocity.y < 0)
{
playerVelocity.y = 0f;
}
在下一节中,我们将分解我们编写的代码,以确保在编辑器中角色移动脚本分配之前的彻底解释。
如果您回想起本章“脚本化角色移动”部分的开始,穿过水面是我们想要设置的一种移动方式之一。我们需要检查角色是否在水中,以便知道如何设置该逻辑。我们将使用来自物理库的Raycast方法,该方法需要参数,如以下图 4.23中的辅助提示所示。

图 4.23:Physics.Raycast 参数
当使用 Raycast 时,其参数如下:origin、direction、hitInfo、maxDistance和layerMask:
-
我们将原点定义为这个 GameObject 的位置加上向上方向的一个单位
-
方向是向下方向
-
hitInfo被保存为名为
hit的RayCastHit -
maxDistance设置为 2 个单位
-
layerMask设置为
waterLayer
为了测试这个,创建一个立方体,并在检查器中选择Water作为其层值。我们将在底部的controller.Move部分调用这个waterLayer检查。
// Check for water
standingInWater = Physics.Raycast(transform.position + Vector3.up, Vector3.down, out RaycastHit hit, 2f, waterLayer);
接下来的部分是输入系统正在读取我们组合的移动值。movement变量是一个Vector2,只有x和y。所以,我们需要操纵这个变量,以确保它对 3D 移动有意义。
// read in the values of the input action assigned to this script
Vector2 movement = movementControl.action.ReadValue<Vector2>();
我们创建一个Vector3,并将读取值中的x和y放置进去,同时保持Vector3的y值为0。
// Use the values from the inputs and put them into a vector3, leaving up blank
Vector3 move = new Vector3(movement.x, 0, movement.y);
现在,我们需要考虑角色和摄像机的协调。我们有一个move变量,它包含我们需要移动到的movement,但摄像机可能相对于你的角色朝向另一个方向,而不是直向前方。所以,在移动之前,让我们考虑这一点。
// take into account the camera's forward as this needs to be relative to the view of the camera
move = cameraMainTransform.forward * move.z + cameraMainTransform.right * move.x;
然后我们再次将那个y值置零。如果我们稍后要实现跳跃,我们需要将这个值从0改为不同的值,这取决于跳跃的高度。
// zero out that y value just in case ;)
move.y = 0.0f;
好了,现在是我们移动角色的时候了。我们已经考虑了与摄像机、角色和地形类型可能出现的所有可能问题。标准的 Unity 角色控制器有一个名为Move的方法。这个方法接受一个单一参数,一个Vector3。这告诉角色朝哪个方向移动。我们需要利用一些东西。他们移动得多快?他们在水中吗?我们在这里使用的新东西叫做三元运算符。
在我们进入下一行代码之前,让我们先稍微解释一下。这是一个三元函数。这里所说的内容是:
If standingInWater is true, this value is whatever the value of waterSlowFactor is. Otherwise, this will be 1f.
(standingInWater ? waterSlowFactor : 1f)
这很方便!我们可以通过可调值轻松地减慢角色的速度,如果我们不在水中,她将以我们已设计的常规速度移动。
controller.Move(move * Time.deltaTime * playerSpeed * (standingInWater ? waterSlowFactor : 1f));
我们在这个类的顶部定义了重力,并将速度设置为重力值乘以时间变化,以考虑帧率。除非 Myvari 由于更新函数顶部的if语句而没有接地,否则这不会计算。该语句将velocity.y设置为0,如果它小于0并且isGrounded。
playerVelocity.y += gravityValue * Time.deltaTime;
controller.Move(playerVelocity * Time.deltaTime);
在这里,我们调用两个方法来处理旋转和动画状态。
HandleRotation(movement);
HandleAnimation(movement);
方法
为了尽可能保持更新循环的简洁性,我们将处理旋转和动画从Update函数中重构出来。
重构是重新组织现有代码以使其更易于阅读的过程。它在更新时运行,但每个方法只调用一行。我们最初想讨论的方法是HandleAnimation方法。这个方法接受一个Vector2作为输入,这是从输入系统读取的Vector2的直接输入。我们只关心单个动画参数isWalking。
我们首先获取布尔值在其当前状态下的值,并将其存储在局部变量中。然后我们检查输入移动中的任意一个向量是否非零,以及isWalking当前是否为false。如果是这样,我们将动画器布尔值设置为true。否则,我们将其设置为false。当这个布尔值改变时,它将在控制器中更新并设置适当的动画状态。
void HandleAnimation(Vector2 movement)
{
bool isWalking = animator.GetBool("isWalking");
if (movement != Vector2.zero && !isWalking)
{
animator.SetBool("isWalking", true);
}
else if (!(movement != Vector2.zero) && isWalking)
{
animator.SetBool("isWalking", false);
}
}
这是我们在这里使用的最先进的方法。我们认为,尽可能经常地走出我们的舒适区是一个明智的想法,以继续成长。我们将逐步讲解这个过程,如果现在它还不清楚,让它慢慢消化,你的大脑会处理它。这里有三个动作在进行。我们需要找到我们想要旋转到的角度,获取一个旋转值,然后旋转!
首先,targetAngle 正在执行一个名为 Mathf.Atan2 的 Mathf 方法。Mathf.Atan2 是一个反正切方法,它允许你根据你想要旋转到的目标位置找到角度。这是一个有趣的方法,在游戏中对于 3D 应用程序中角色的旋转非常有用。问题是,我们还需要再次考虑相机。Mathf.Atan2 返回弧度,因此我们需要将其乘以弧度到度的常数,然后加上相机的 y 角度。这是从角色角度的偏移量。
接下来,我们然后从目标角度创建一个四元数,在当前相机的 y 轴上。这将允许我们得到一个我们将需要到达的角度,而不用担心会发生 万向节锁。万向节锁是指由于一个轴偏离中心 90 度,两个轴在旋转中卡住。四元数不会受到万向节锁的影响,这就是为什么我们最终从欧拉角转换到四元数。
根据定义,欧拉角是相对于一个固定坐标系定向的。这是我们表示游戏中我们所在的角度的方式,其中参考是从 0、0、0 的旋转导入。如果你在 y 轴上旋转角色 90 度,它表示为 0、90、0 在那个 GameObject 的变换的旋转字段中。这些值是欧拉角。
最后,我们需要过渡到那个旋转值。我们通过一个 Slerp 来做这件事。这代表一个 球面插值。当处理旋转时,最好使用 Slerp 方法。参数是我们的当前旋转、我们刚刚创建的新旋转,以及旋转到那个新位置所需的时间。我们使这个旋转速度公开可用,这样我们就可以在飞行中更改它以获得感觉最好的变量。
Void HandleRotation(Vector2 movement)
{
if (movement != Vector2.zero)
{
float targetAngle = Mathf.Atan2(movement.x, movement.y) * Mathf.Rad2Deg + cameraMainTransform.eulerAngles.y;
Quaternion rotation = Quaternion.Euler(0.0f, targetAngle, 0.0f);
transform.rotation = Quaternion.Slerp(transform.rotation, rotation, rotFactorPerFrame * Time.deltaTime);
}
}
在完成这些之后,你的角色现在有了移动和旋转的能力。这是构建一个环境驱动、叙事驱动的探索游戏的一个很好的第一步。让我们总结一下本章涵盖了哪些内容。
摘要
在本章中,我们涵盖了大量的角色信息。我们讨论了设计、建模和绑定、角色控制器、刚体、在 Unity 中工作以及编写移动控制器脚本。
设计始终归结为“为什么?”你应该从本章中了解到,你角色的“为什么”以及他们的动机将有助于确保一个独特且易于引起共鸣的角色。建模和绑定很大程度上取决于你需要进行的建模类型。我们介绍了一些关键方法来帮助你将动画首先考虑在建模中。这也适用于绑定。动画将是最终阶段,动画越容易正确执行,你和你玩家将发现更好的整体体验。动画往往会在发布前一直进行修改。认真对待绑定设计,因为一旦动画开始制作,做出更改可能会导致需要重新制作动画。
我们意识到,Unity 内置的角色控制器对我们来说最有意义,因为我们不需要 Myvari 被物理效果抛来抛去,比如拉杆效果。然后我们进入 Unity,导入 Myvari,并检查了获取输入以及她的动画所需的组件。最后,我们完成了关于移动和旋转的角色脚本的检查。
在下一章中,我们将探讨环境、地形以及一个名为 ProBuilder 的工具。
加入我们的 Discord 社群!
在撰写这本书的时候,我们在 Unity 服务器上有超过 200 名 Unity 专业人士,我们正在不断适应,添加新的频道以促进关于关键主题的讨论,例如 C# 编程、游戏机制、游戏 UI、动画、3D 游戏、音效和效果,以及一个专门用于 Packt 作者与本书读者建立联系的频道。
告诉我们到目前为止你所取得的进展以及你构思的与这本书一起构建的游戏想法。你永远不知道——你可能会在今天与服务器上的某人合作,组建你的迷你团队。

第五章:环境
当玩家进入一个主要由环境驱动的叙事游戏时,你需要确保他们可能提出的大部分问题都能得到解答。我们将花费时间在环境发展的三个主要结构上——环境设计、布局和迭代。这应该听起来与我们之前在第四章中做的角色工作很熟悉!幸运的是,在环境方面与角色有关的一些差异,我们将在本章中详细讨论。到本章结束时,你将解决足够的环境问题,了解我们是如何设计叙事的,同时也能着手自己的工作。让我们分解这些主题,以便你能一窥本章内容:
-
设计——草图、情绪板和布局
-
布局——网格布局、Unity 地形、Unity Probuilder
-
迭代过程的工作
-
环境设计
游戏中的环境与角色一样重要。我们需要深入思考我们的选择,考虑环境如何与游戏的主题和叙事保持一致。这就是我们与概念艺术家和设计师坐下来提出困难问题的时刻。尽可能详细地工作,以发展环境各部分的目的。
构想一个环境需要从某个地方开始。我们是从草图开始的。一开始我们就有了想要的环境感觉,所以我们决定快速勾勒出一些概念草图。草图完成后,我们制作了一些情绪板,以更好地定义风格。
一旦我们对风格和总体概念感到满意,我们接下来想要在以下三个阶段工作,以设定关键点的基调:
-
草图
-
情绪板
-
布局
让我们详细看看这些阶段,从草图开始。
草图
你可能对想要的环境外观有一些强烈的想法。就像在其他概念阶段一样,你需要花费大量时间问“为什么?”。这个问题将帮助你定义环境的背景,以确保体验能够融合在一起。
要进行草图绘制,你可以采用几种方法。笔和纸非常适合快速绘制草图。有人可能会有一个很好的想法,在餐厅的餐巾纸上画!如果你在电脑前,如果你有订阅,可以使用 Photoshop,或者尝试免费的替代品,如 Krita 或 GIMP。花些时间绘制出建筑、大致形状和感觉。每一张草图都会让你更接近最终产品的样子。在每一张快速草图之后,与你自己或团队进行简短交谈,以确定你是否需要就环境相关的更多“为什么”问题进行提问。所需的草图数量将根据你传达玩家想要体验的情感的自信程度而有所不同。如果你不能完全描述每一部分的推理,那么继续绘制草图,并继续提问“为什么?”。随着时间的推移,细节将足够丰富,可以继续前进。
下面,在 图 5.1 中,是一系列图像,我将简要解释它们为我们的游戏垂直切片的环境设计带来了什么。我们真的想描绘环境的总体类型,这意味着我们在开发时不必担心细微的定义。我们不是在寻找遗迹的建筑或环境的植物。最初,我们 100%确信我们将在山上如何构建遗迹,但我们需要在我们的草图中进行搜索,以找出什么感觉是正确的。我们发现我们想要一个偏远的山区,植被茂密到几乎在整个过程中都感觉像洞穴。这逐渐形成了一种感觉,你实际上并不在地球上,而是在一个与地球相似度足够高的星球上,自然形状让你感到舒适。

图 5.1:草图示例
当你在教程中前进时,我们希望当你揭示 Myvari 的过去时,最后一节能带来该区域感觉的巨大变化。这最初在 图 5.1 的右下角概念草图中有展示。从这个点开始,我们知道我们需要推动建筑问题,并定义出中等的形状。

图 5.2:最后的谜题区域
通过进一步发展上一节的内容,我们知道当世界转移到 Myvari 的领域时,我们需要在主题视觉上有一个鲜明的对比。查看上一节中的 图 5.2 中的图像差异,主要差异在于白天与夜晚。这本身就是一个很大的变化,但我们还希望在地面上和树木消失的同时,将建筑恢复到其昔日的辉煌,并引入反射的静水,反射天空中的星星,从而真正地开辟这个空间作为一个新的维度。
在草图绘制出你的环境概念之后,你想要做的是利用从草图中学到的知识来构建一个情感板。在这里,你可以为未来作品的创作确立参考。
情感板
情感板是一组图像拼贴,可以描绘出一个地区的风格和情感。互联网上有成千上万的人绘制的、渲染的、草图或甚至摄影的图像,这些图像可能足够接近你的风格和基调,你可以将它们组合起来,为玩家提供更多体验的灵感。
如果你有一些明确的草图,那么这是你在情感板上大放异彩的时候。花些时间去寻找与建筑相似的特征和感觉,制作出反映环境情感应该感觉如何的拼贴。这将为你在这个环境旅程的建模部分定义色彩调色板。
对于我们的项目,我们有几个主要要求。我们想要一个多山的丛林,给游戏带来一种古老的魔法幻想文明的感觉。即使你搜索“魔法幻想古老丛林废墟”,也有很大可能性无法通过参考找到你需要的精确内容。相反,你应该分解每个区域的主要功能,并制作一个情感板。在我们的两个例子中,我们将讨论洞穴和废墟的情感板。

图 5.3:洞穴情感板示例
在上面的图 5.3中,我们关注了洞穴的主要功能。感觉渺小是怎样的?雾气和照明的氛围倾向是什么?这即使在地球上,也许是在我们地球的一个未开发区域,也能给人一种外星环境的感觉。

图 5.4:废墟情感板示例
在图 5.4中,我们想要将废墟在奇幻文明中的功能进行分解。一个以自然为中心的魔法生物会追求什么样的形状?颜色是如何相互融合的?
情感板需要回答情感和基调。当你完成对情感的搜索时,你会知道每个关键区域都有一个最适合该区域所需传达情感的马赛克。在我们的案例中,我们需要感受洞穴与废墟之间的区别。你会注意到,即使没有看到所有更细致的图像细节,两者在情感上也有很大的不同。
在草图之后开始这个步骤是有意义的,如果你可以通过这一步得到问题的答案。然而,如果你发现草图只是在提出更多问题,而不是定义你想要的风格,那么情感板应该是最先考虑的。寻找建筑图像往往能带来很多清晰度,关于你想要构思的内容。
场景布置
在快速勾勒出想法和感受以定义“为什么”的问题之后,你接着制作了一个情绪板来进一步巩固环境的感受。下一步是将之前定义的形状和情绪用于推动你的叙事和机制进入游戏的各个阶段。你带玩家去的第一个地点需要迅速回答很多问题。幸运的是,你在这一阶段之前花时间回答了尽可能多的问题。你现在可以自信地安排你的叙事设计了。对于我们这个项目,我们知道我们需要在每个你进步的区域尽可能多地解释 Myvari 的过去。
当你在构建一个场景时,尽可能将自己置于场景中。花时间通过场景,并确保你的主要问题在之前部分得到了你预期的回答。现在,让我们尝试一个新的实验:通过新玩家的视角想象你创造的经验。尝试感受他们作为 3D 游戏的新玩家可能会感受到什么。是否有足够的提示来描述他们的体验?然后从经验丰富的玩家的角度看待它;添加任何其他东西是否会损害这些高级用户?
这可能需要几个迭代。对这个步骤要有耐心,并完成这个阶段以确保你能解释角色的假设动作。
这是个好时机向别人展示你的设计,看看他们能提出哪些未经提示的问题;我们自己在没有过多思考的情况下能回答的问题真是令人惊讶。在每个阶段都有一双新的眼睛将揭示需要更多细节的地方。
经过一段时间,每个阶段你需要的东西的图片将更加清晰,然后你可以将你的阶段带入勾勒阶段。
勾勒
现在你已经尽可能多地处理了所有概念,你应该非常清楚什么会使你的环境适合叙事和角色。在这个时候,下一步将是尽可能多地完成“勾勒”;勾勒的目的是在 Unity 中将所有部件组合在一起,以实现我们在之前阶段努力定义的经验。
现在你已经熟悉并适应了整个级别的每个部分,你可以谈论每个部分的氛围和基调,并从概念中调整整体形状。为了勾勒出一个级别,我们将使用我们可用的几个工具;Unity 地形、基本形状和 Unity Probuilder 将帮助我们放下基本的环境部件。
Unity 地形
在 Unity 中使用地形工具是非常有能力的。它非常容易上手,并能快速制作出美丽的景观。要开始,让我们创建一个地形实体。在创建地形实体之后,我们将浏览设置、绘画和 Unity 为我们提供的植被,这些都是我们创建地形的工具。
创建地形
与地形一起工作的第一步是创建一个。主要有两种方法可以做到这一点。
一种方法是点击GameObject菜单,然后3D Object,然后Terrain,如图图 5.5所示。
另一种开始的方法是在层次结构中右键单击一个空白区域,然后选择3D Object,然后选择Terrain。

图 5.5:创建地形实体
这两种选项都会在场景中创建一个地形 GameObject,其全局坐标为0,0,0,默认情况下在x和z平面上向外扩展 1000 个单位。这个值可能不是你需要的,所以让我们来看看地形设置。
地形设置
在我们的垂直切片中,我们将使用默认的单位,因为这些是我们最终需要的用于感知尺寸的单位。这可能不适用于你的游戏。地形的一个很好的特点是它可以轻松地连接到相邻的地形地块。

图 5.6:创建相邻的地形
地形组件中的第一个选项是创建相邻地形按钮。当选择此选项时,你将看到相邻地块的轮廓,如图图 5.6所示。如果你点击这些方块中的任何一个,地形将创建一个新的与主地形资产链接的地形资产。
现在你已经了解了地形工具如何轻松地连接到其他地形,你现在可以以每个地块的大小来考虑你的地形设置。可能你的地形只需要 500 个单位长度,200 个单位宽度。这些的公因数是 100 个单位,所以你可以按照下面的图 5.7中的设置来设置你的参数。

图 5.7:网格分辨率设置
如果你计划在你的地形上使用草地或细节,确保你的地形是正方形的。如果宽度与长度不同,细节刷将很难有序地放置广告牌。
点击开放的方块将用另一个地形地块填充它们。点击多个可能会得到类似于图 5.8的地形。

图 5.8:添加相邻的地块
这让你有自由添加一些不太大的分支,如果你只需要在你不期望需要它的地方多一点点地形。
在我们的例子中,我们知道我们只需要一个 1000x1000 的方块,所以我们坚持使用默认的大小。我们的整个垂直切片将在一个场景中完成,使用这个默认大小以方便设置。
一旦你的地形按所需的比例设置并设置了大小,你可能还需要给你的地形添加一些细节。尽管无限平坦的拉伸平面在其自身的方式中很有趣,但你的概念很可能确实有一些山丘或山脉。让我们开始在那里绘制这些。
地形绘制
要获得那些美丽的山脉和丘陵,我们需要影响地形的几何形状。绘制地形工具正是为此而设计的。你可以在地形对象检查器中找到这个工具按钮,它位于第二个可用选项中,如图 5.9 所示。

图 5.9:刷子选项
你的刷子选项位于工具选项下方,是一个下拉菜单,它将改变刷子的功能。要查看刷子选项列表,请点击如图 5.10 所示的下拉菜单。

图 5.10:绘制选项
我们将详细介绍它们的精确功能,但我建议你花点时间,通过尝试不同的刷子来制作一个你感到舒适的地形。没有什么比经验更能让你了解它们在地形上的作用了。
升降地形
在制作地形时,这个工具将是你的得力助手。根据你地形的大小以及你需要对地形进行修改的规模,你的刷子大小将根据你的需求而有所不同。幸运的是,有一个很好的指示器,如图 5.11 所示,在你确认更改并点击之前,它会告诉你更改的大小和形状。

图 5.11:刷子在不同高度上的可视化
当你选择升降地形刷子时,它会显示一个描述框,解释如果你点击地形,刷子将根据刷子形状提升地形;如果你在点击刷子时按住Shift键,这将根据刷子形状降低地形。如果你尝试在平坦部分降低地形,你会注意到它不会低于 0。如果你计划在地形中制作凹痕以可能放入水,这可能会成为一个问题;有一种方法可以绕过这个问题,我们将在设置高度部分稍后介绍。
绘制洞口
在选择绘制洞口工具并花一点时间用洞口进行点击后,这个工具可能不会立即显得是最有用的与地形相关的工具,因为它只是删除地形。除了它只是挖洞之外,它还产生了锯齿状的边缘,这些边缘与地形其余部分平滑的形状不太搭配!
但并非一切都已失去!如果你需要将洞穴作为地形建造,而这个地形没有设计来处理会导致地形顶点重叠的凹形形状,这个工具对你来说非常棒。
设计包含所需洞穴系统的 3D 网格并将其放置在地形下方,然后在地形中挖一个洞以进入,这是一种常见做法。
我们将在稍后的部分更详细地介绍 3D 网格,但这里有一个简单的解释:3D 网格是一组顶点,它们形成多边形,我们使用它们来可视化 3D 空间。
地形只是一个被操作的平坦区域;它不是你可以用铲子挖掘的地面。如果你想在地里挖洞,你需要挖一个洞,然后在地面下方构建一个网格。这将留下一些参差不齐的边缘,如图 5.12 所示。

图 5.12:地形中涂鸦前后对比示例
你可以用岩石或其他通向洞穴系统的网格来覆盖那些参差不齐的边缘。
涂鸦纹理
现在你可能已经玩得足够多了,以至于你有一些山丘、高原和其他各种灰色地形相关材料。你可能想在生活中添加一些色彩,幸运的是,有一个简单的方法可以做到这一点!你可以在网上搜索可重复使用的纹理,或者你可以使用项目中提供的纹理来设置这个。
当你第一次选择涂鸦纹理工具时,没有什么可以涂鸦的,因为你需要创建层来涂鸦。你的地形层将保持空白,直到你创建一个,所以让我们先做这个。

图 5.13:编辑用于涂鸦的地形层
在地形层的右下角,如图 5.13所示,有一个标有编辑地形层的按钮。如果你选择这个按钮,你可以创建或添加一个层。如果你之前没有创建过层,你可以点击添加层选项,但将没有选项来填充它。相反,让我们点击创建层。这将弹出一个对话框来选择纹理。这里的一个小技巧是给你的纹理起一个容易搜索的名字,以防你有很多纹理要筛选。例如,你可以用前缀TT_给你的地形纹理命名,比如TT_Grass代表草地。然后,当对话框打开时,你可以在搜索栏中输入TT,它将只显示地形纹理。这个技巧可以在整个项目中使用,因为大多数可用的选项中都有搜索栏来选择一个资产来填充角色。
当你选择纹理时,它将创建一个包含你选择的纹理和一些材质选项的地形层资产。
图 5.14显示了地形层的一个示例。

图 5.14:地形层示例
你在这里想要看到的首要属性是平铺设置。根据游戏规模的不同,你可能需要增加或减少缩放以避免看到纹理的平铺。如果你查看下面的图 5.15,你可以看到纹理是如何反复平铺的。从当前相机的位置来看,这看起来很糟糕,但在游戏中,它将更近,这使我们能够防止平铺变得过于明显。最有趣的部分是,它与地形没有连接,所以如果你对图层资产进行了更改,它将立即更新地形。这使得在快速工作并获得你想要的地形外观的想法方面变得很方便,而你可以在稍后添加法线贴图和金属或平滑度元素。
第二件事是整个地形将用这个纹理进行绘制。当你添加另一个图层时,你将能够利用相同的画笔形状和大小来绘制其他图层到地面上。
技术上有趣!
地形图层的工作方式是 Unity 为在地图上绘制的每个纹理创建一个纹理图。如果你有四个图层,每个图层对应于纹理中的四个通道:红色(R)、绿色(G)、蓝色(B)和透明度(A)。如果你有五个,地形将获得一个新的纹理,并将其添加到另一个纹理的 R 通道中。由于这种情况,出于性能原因,限制每个平铺为四个纹理!
在熟悉了通过图层工作之后,用你的角色测试小部分内容是个好主意,以确保图层的缩放对于游戏规模本身是有意义的,同时考虑到还有其他噪声因素会破坏纹理平铺,例如草地、岩石、树木或你环境中放置的任何其他东西。

图 5.15:绘制时测试画笔强度
如果角色在这一点上关于纹理与缩放的关系看起来不错,让我们继续下一个选项。
设置高度
当使用较低的高度工具工作时,你可能已经注意到它不会低于零点。如果你知道你将在零以下工作,这是非常常见的,那么开始的工作流程是:在做出任何更改之前,将地形的设置高度设置为可能适合你需要的低于零的高度。
你需要下降 200 个单位吗?如果是这样,将地形 GameObject 的位置y设置为-200,如图 5.16 中用红色突出显示的步骤1。然后选择绘制地形选项,同时选择设置高度下拉菜单,如图 5.16 中用红色标记的2。之后,值将是-200,所以将其设置为0,然后使用图 5.16 中用红色标记的平整按钮将其平整。

图 5.16:设置高度以允许低于世界 0
这将使地形在视觉上回到 0,0,0 的位置,并保持偏移量不变,以便您可以降低地形低于该标记。这对于制作沼泽、洞穴和河流非常有效。
平滑高度
平滑是一个简单的工具。有时,您可能只需要稍微平滑地形,因为放置在上面的噪声可能已经失控,或者您需要平滑一条路径,以便玩家角色行走并帮助引导角色的移动。
作为简单的例子,请看下面的图 5.17。

图 5.17:平滑地形
这是一种相当极端的平滑版本,但在平滑之前两者看起来是相同的。您还可以使用带有噪声笔刷的平滑来以非均匀的方式平滑地形,从而给地形带来侵蚀的外观。
地形盖章
盖章工具被用作 3D 盖章!如果您需要特定的地形特征,您将编写一个高度图来盖章地形。您将高度图添加到笔刷中,然后将其用于地形。
此工具的主要用例之一是您可以获取已经证明在地形上看起来很好的预编写高度图。如果您想找到好的山脉和丘陵,您可以在资产商店中搜索它们,并将有示例供您开始。这将大大加快进程。这可能不是您需要的确切内容,但每一步都是进步。
绘制树木
当您选择绘制树木工具时,它将具有图 5.18中所示选项。

图 5.18:绘制树木地形模式
要开始,点击编辑树木按钮,并添加树木。即使您喜欢的网格不是树木,您也可以添加它!在图 5.19中可以看到一个警告,如果您的网格没有正确构建以在地面上的树木位置,则会弹出。它看起来像这样:

图 5.19:定义的树木和 LOD 组警告
幸运的是,在资产商店中有一个由SpeedTree提供的免费资产,您可以下载以获取如何正确组装用于绘制树木工具的树的绝佳示例。
绘制细节
最后,我们有绘制细节。在这里,您可以选择添加一个细节纹理,它将在四边形上渲染,或者您可以使用细节网格来创建自己的网格。以下是一个简单的草地纹理被用于并绘制在平坦地形上的示例:

图 5.20:草地作为细节
绘制细节,如草地,有助于打破地面纹理,如上图图 5.20所示。这些项目也可能受到风区的影响,这是可以添加到地形对象中的另一个组件。如果您想更进一步,请查阅第十章,声音效果,我们将添加环境声音和其他小的抛光细节,以及第十二章,收尾工作,为地形注入生命。
3D 几何
现在您已经设置了地形,您将需要增强地形以用于建筑设计或构建洞穴系统。您需要利用 3D 数字内容创作(3D DCC)工具来构建您环境的网格。还有一个构建块状阶段的选择,那就是 Unity 的 ProBuilder。在我们的案例中,我们将使用 ProBuilder 并创建我们自己的几何形状以定义自定义形状,以表示环境的特定建筑部分。
让我们深入探讨 ProBuilder 和自定义网格对您的环境块状建模意味着什么。

图 5.21:Unity 的 ProBuilder 和自定义网格的示例,来自我们的 3D DCC
ProBuilder
根据 Unity 文档,ProBuilder 定义如下:
您可以使用 ProBuilder 包中提供的操作和工具在 Unity 中构建、编辑和纹理自定义几何形状。您还可以使用 ProBuilder 帮助进行场景级别设计、原型设计、碰撞网格和游戏测试。
ProBuilder 可以快速设置您的场景,带有可碰撞表面,以便轻松可视化您的环境。要开始使用此工具,我们将介绍一些起始步骤,您可以在其中创建自己的场景以便熟悉。我们将介绍安装、创建 ProBuilder 形状、编辑,以及 ProBuilder 中的一些常用工具。
安装 ProBuilder
要安装 ProBuilder,请打开包管理器并转到 图 5.22 中显示的 Unity 注册表。选择 ProBuilder,然后下载并安装它。

图 5.22:Unity 注册表包管理器
安装后,您需要访问 工具 菜单以打开 图 5.23 以下显示的 ProBuilder 窗口。

图 5.23:Probuilder 窗口路径
这将打开一个带有许多选项的浮动窗口。要开始,让我们将窗口停靠在场景窗口的左侧。这是一个个人偏好,但我们喜欢能够使用 ProBuilder 并轻松选择层次结构中的项目。现在我们已经设置好了,让我们来看看菜单中的颜色。

图 5.24:Probuilder 对象模式
在 图 5.24 上,我们目前正在查看 ProBuilder 的 对象 模式,在这个菜单中只有三种颜色可用,但有一个第四种颜色,我们将在 常用 ProBuilder 工具 部分中介绍。我们目前看到的这三种颜色以特定方式使用,以帮助轻松处理所有选项。它们是这样工作的:
-
橙色:对象和窗口工具
-
蓝色:基于选择的函数
-
绿色:影响所选整个形状的网格编辑工具
现在我们已经了解了这些工具的用途,让我们开始构建我们的第一个形状。
创建 ProBuilder 形状
打开一个新的场景,并从 ProBuilder 菜单创建一个新的形状。这将揭示 场景 视图窗口右下角的一个小窗口,如 图 5.23 所示。你将有几个关于你想要创建的形状类型的选项。我们将选择 平面 选项,以便我们有一个可以附加到形状上的东西。你可以在下面的 图 5.25 中找到它。

图 5.25:创建形状子菜单
选择平面后,在场景中左键单击并拖动以创建一个平面。暂时不用担心它的大小;只需创建平面,我们将在下一步进行编辑。
现在,在层次结构中,如果平面没有被选中,请继续选中它。在检查器中,我们将变换设置为 0,0,0,使其在场景中居中于 0。然后,转到 ProBuilder 脚本并更改大小为 80,0,80。这将为我们提供足够的空间来在场景中玩任何我们想要的形状。完成这些步骤后,检查器应该看起来与 图 5.26 类似。

图 5.26:位置和形状属性检查器窗口
创建平面后,让我们制作一个立方体。创建一个新的形状,并在场景中的交互式工具中选择一个立方体形状。左键单击并拖动以创建你想要的任何基础形状。释放左键,然后你可以向上拖动以给立方体其高度。现在,点击你想要的高度,完成制作立方体。一旦完成,让我们向立方体添加一个额外的形状,楼梯。目标是制作一组楼梯,使角色能够爬到立方体的顶部。如果你的楼梯看起来很奇怪,不要担心;只需删除它们并再次尝试,直到接近为止。不用担心做得完美;我们将在下一步进行形状编辑。
编辑形状
有可能当你制作楼梯时,它们看起来像下面的 图 5.27。

图 5.27:楼梯放置问题
虽然你可能不希望这样,但它就是这样出现的。幸运的是,ProBuilder 的编辑部分非常强大。如果你将鼠标悬停在楼梯的面上,应该有一个指向上、下、左、右的蓝色箭头,分别对应于该面的中心正方形,如前图所示。
如果你悬停时没有这个,这意味着你可能已经取消选择了楼梯,而 ProBuilder 认为你不再需要编辑基础形状。这可以通过在场景中选择另一个项目然后重新选择楼梯来轻松修复。然后,在检查器和 ProBuilder 脚本中,有一个编辑形状按钮。选择这个按钮将再次为你提供基本编辑功能。
点击并拖动中间的正方形将允许您移动形状的这个面。点击箭头将重新定位整个形状。这对我们的楼梯非常有用。图 5.27中突出显示的黄色箭头将形状定位在楼梯的后面,这将使最上面的楼梯面与立方体接触。这正是我们想要的,所以我们选择了它,然后使用中间的正方形重新定义形状,使其在这个例子中看起来最好。
尽管这些工具在绘制块时非常强大,但我们还可以进一步讨论组件操纵工具。
常见 ProBuilder 工具
我们已经创建了一些形状,并编辑了它们的整体状态和形状,以获得结构的基本构建块。为了获得更多中等形状,我们需要对这些形状的组件进行工作。回顾第一章,三维入门,3D 对象的组件结构是顶点、边和面。当安装 ProBuilder 时,这些组件在场景视口顶部的图标形状表示,如图图 5.28所示。

图 5.28:组件选择工具
从左到右,我们有对象、顶点、边,最后是面选择。如果您选择其中之一,您将能够选择在层次结构中选择的 ProBuilder 形状的这些组件类型。这也会改变 ProBuilder 工具集中的可用选项。将此与图 5.29中的选项以及图 5.24中的选项进行比较,您会看到我们添加了红色到我们的可用选项,其他颜色在其选项中也有所变化。

图 5.29:组件菜单选项
可用的红色选项是针对每个组件的。在这种情况下,我们选择了顶点操纵器;组件工具的选项与顶点相关。关于选择边或面,情况也相同。花些时间循环查看每个组件的可用选项。当您到达面时,让我们停下来,因为有一个非常常用的工具——挤出工具。
将面挤出会复制顶点并保持面与挤出几何体连接,从而从选定的面中挤出几何体。这是一个非常强大且快速的工具体现,可以构建许多形状或添加细节。我们将执行两种版本,以展示其工作原理。这些是挤出和内嵌。
要进行挤出,在场景窗口顶部选择面组件选项,然后选择楼梯旁边的盒子上的一个面。按W键进入变换工具。在按住Shift键的同时,左键单击并拖动向上的箭头。你应该会得到一个向上的挤出!它应该看起来像下面的图 5.30。

图 5.30:挤出工具
拉伸是拉出所选面的顶部盒子。你现在可以操纵这个面,使其成为你想要的任何形状。我们并不是特别喜欢所有的 90 度角,所以我们按了R键并从中部缩放,给它增添一点个性。
现在,内嵌很有趣,因为它们是一组特定的拉伸,将面拉回当前形状。这对于添加快速细节非常棒。我们将通过选择面对摄像机的侧面面,按字母R键使用缩放工具,同时按住Shift键,左击中心灰色盒子从所有方向进行缩放来实现这一点。完成之后,我们将按W键回到变换工具,再次按住Shift键,拉动蓝色箭头以拉入内嵌。以下图 5.31包括了在同一面上创建内嵌的步骤。

图 5.31:内嵌示例
玩转每个组件的工具。你会发现它们都是为了帮助加快工作流程,让你尽可能快地完成布局。有时,你可能知道需要特定的形状,但无法使用 ProBuilder 工具实现。在这种情况下,你可能需要使用外部工具,如 Autodesk Maya 或 Blender 来达到所需的视觉效果。我们通常将这些对象称为预制形状。
预制基本形状
如果你已经对你的环境所需的形状有了很好的想法,你可能已经设置了正确比例的连接环境部件。ProBuilder 在基本形状方面非常出色,但你可能需要一些特定于你游戏的建筑。如果你有条件,直接创建它并使用网格可能更容易,而不是使用 ProBuilder。
在某些情况下,你可能已经创建了可以与 ProBuilder 一样好的形状,你只需要将它们导入 Unity 并放置到你想要的位置。这可能会在加速布局阶段非常有用。
在我们的场景中,你会看到我们使用了所有三种(地形、ProBuilder 和预制形状)来构建一个连贯的集成场景。这主要是因为我们的艺术家在 DCC 和 ProBuilder 中创建某些模型的速度。然而,有时,我们可能只需要一个基本的拉伸块来使场景在布局上显得合理。
迭代
迭代的过程是通过循环工作的一部分,以达到足够精细的状态,以便继续进行游戏的其它部分。我们将以下图 5.32作为简单的跟踪。这是过程的高级概述,在你多次经历之后,你会为自己和团队创建更多步骤来遵循。现在,让我们先处理一下重点。

图 5.32:迭代过程
我们在上面的过程中已经讨论了几个阶段,例如构思和布局。我们只需要进行细化和测试。细化是在细粒度层面上尽可能接近回答动作所采取的行动。以我们的项目为例,我们需要细化每个谜题与 Myvari 文化的接近程度。我们需要将这一点从布局细化到架构,并确保谜题本身在机制风格上是有意义的,这一点我们将在第六章,交互 和 机制中讨论。如果细粒度层面的答案是有意义的,那么你可以扩大范围进行测试。在测试中,你将全面审视游戏。如果你能的话,通过四处走动来测试游戏的感觉,看看是否还有其他在细化过程中出现的高层次问题。
这就是时间很容易溜走的地方。你需要明确你游戏中的“足够好”是什么意思。游戏开发中最困难的部分之一是意识到发布游戏比让一切都完美更重要。在发布和让它看起来和感觉足够好的过程中保持警惕,然后继续前进。你还有更多的事情要做!
我们在本章的环境章节中只讨论了布局阶段。这是因为布局非常重要,需要花时间通过游戏来感受它。快速失败并根据你或你的朋友玩游戏时的反馈进行修改。当你认为每个部分都足够好时,然后你可以开始导入最终网格,这些网格的形状或位置是你认为正确的空间。一旦完成,再次运行游戏,因为你可能会看到最终网格带来的变化,这些变化在布局网格中之前并不明显。
在场景的多个部分移动时,仔细观察一致性。一旦在场景中移动感觉良好,那么你需要更多地深入到游戏本身的机制中,并进入更多的发展阶段,而不仅仅是艺术方面。未来还会出现更多问题。首先认真对待这些迭代将为你进入游戏下一部分打下坚实的基础。
摘要
我们在本章中已经介绍了很多 Unity 工具。请花些时间熟悉地形工具和 ProBuilder 工具,以便更好地理解它们的工作原理。
从本章中,您获得了多种工具来构建环境的知识。我们花了时间解释如何迭代整个流程,以在您的环境中获得强大的结构感。您学习了如何从设计思维开始构建一个概念。然后,您将这个概念分解成各个部分,并开始逐一展示,最终将环境组合起来,并对其进行迭代,以获得全面的清晰视图。
接下来,我们将探讨如何将您游戏的机制适应到您的环境中。在放置机制交互时,请记住这一章节,因为在开发过程中会有更多的迭代。
第六章:交互和机制
现在我们有一个具有基本移动能力和可以与之交互的环境的角色,让我们看看这个角色应该如何与这个环境交互。Unity 允许我们使用 C#围绕玩家可以与之交互的 GameObject 构建逻辑。这是游戏设计的基础,并有助于通过实际交互讲述故事或体验。
在本章中,你将了解更多关于可以使用 Unity 实现的具体交互和机制。我们将涵盖:
-
游戏循环
-
机制工具箱
-
项目内的交互
-
楼梯
-
环形谜题
-
狭小空间
-
交互体积
-
设计与实现
游戏循环
视频游戏有一个独特的概念,称为游戏循环。正如你可能猜到的,它是在整个体验过程中执行的一系列机制循环。游戏循环本身可能非常短,例如《使命召唤》的多玩家团队死亡匹配。循环看起来可能像这样,目标是杀死比死亡次数更多的敌人:
-
杀死敌人
-
死亡并重生
这不仅仅是那样,如果你是一个职业的《使命召唤》玩家,你可能认为这是对游戏玩法的一种过度概括。然而,最终,这真的是 90%的情况。现在让我们看看 Minecraft 的游戏循环:
-
白天收集资源
-
白天建造
-
夜间生存
我们将简化这一点:有一些特定的情况不在这个循环中,例如白天的爬行者以及降雨,这降低了光照水平,使得实际上变成了夜晚。让我们假设这两个因素不包含在这个研究中。这很有趣,因为这个循环尤其复杂。我的意思是,生存并不总是在这个循环中发生。游戏的大部分时间是 1,然后是 2。只有在夜晚,3,夜间生存,才成为游戏玩法的重要组成部分,这在图 6.1中有视觉表示。核心游戏循环需要尽可能简洁。

图 6.1:Minecraft 游戏循环
看看你最喜欢的游戏,并分析它们的主要游戏循环。你可能会发现存在游戏循环的层级。有时这被称为元进度。在游戏《Hades》中,游戏循环如下:
-
(可选)与 NPC 交谈
-
(在大厅中)选择升级的技能
-
(大厅中)为下一次运行选择武器
-
(在游戏中)战斗
-
(在游戏中)为大厅升级赚取货币
-
(游戏中)升级以增强这次游玩
-
在大厅中死亡并重生
元进度发生在步骤 2。基础生命值和伤害升级使进一步进步变得稍微容易一些。这在以技能掌握和通过死亡进行游戏进度的 roguelike 流派中是一个常见因素。
你会注意到在《使命召唤》循环中,我们没有提到元进度,尽管这款游戏中存在大量的元进度。这是因为元进度本质上只是装饰性的。在《使命召唤》中,你不需要在比赛之间做任何改变。在《使命召唤》中获得的任何装备都将与拥有相同模组的其他玩家的装备相同。如果你让一个玩了 1000 小时的玩家与一个装备完全相同的玩家对战,那结果将只取决于技能。然而,在《冥界神殿》中,你必须花费点数来升级,才能真正完成游戏。
这些循环很有趣,但我们应该花些时间深入研究构成这些循环的交互。在下一节中,我们将单独介绍一系列游戏机制。
机制工具箱
交互是通过机制采取的行动。例如,使用物品的机制可以用来拉动杠杆、按按钮或使用电话。这三个例子都是交互;机制允许玩家通过按钮点击与物品交互。如果我们只能以这种方式与某物交互,我们将只有很少的游戏类型可以玩。幸运的是,我们有广阔的机制世界可供我们使用来创造交互。通过使用交互,我们可以设计出惊人的体验!
为了开始这一章,我们认为提供一个来自这些机制的机制列表和一些交互列表是个好主意。我们无法涵盖每一个机制,但我们将探讨一些主要概念,以获得对机制景观的良好感觉。
我们在这里提供的是对机制及其如何被看待的理解。如果你对此感兴趣,花些时间阅读几位不同作者关于这个主题的作品,因为他们对机制的看法可能与我们解释的不同。我们看待机制的方式是它们是体验运动的层次。有一些核心概念可以叠加在一起形成交互。这些包括:
-
资源管理
-
风险与回报
-
空间意识
-
收集
-
研究
-
局限性
继续阅读,以了解这些游戏设计的模块化核心概念。
资源管理
你可能知道这是实时策略游戏的主要机制,或者称为RTS。例如,《星际争霸》、《帝国时代》和《全面战争》都是流行的以资源管理为重点的游戏。这里的理念是,你需要收集和花费有限的资源在能帮助你赢得比赛的东西上。这可能是一支军队的士兵或使你的士兵变得更强大的实验。一个非战斗相关的场景是城市建设者。你需要监控你城市的人民,并为他们建造东西以使他们快乐,同时管理从他们那里流入的钱。
风险与回报
这种机制在许多以战斗为导向的游戏中被使用。它通常以冷却时间的形式出现。以流行的游戏《英雄联盟》为例,你想现在就使用你的终极技能吗?这可能会消灭一个敌人并给你带来重大优势。然而,如果你失误,这也可能让你处于劣势,因为敌人会知道你少了一个可以使用的力量。这是风险与回报的概念。这种概念最简单的形式体现在《超级马里奥兄弟》中。你应该尝试那些难以触及的硬币吗?你想要这些分数来获得额外生命,但与此同时,如果你不正确跳跃,你可能会掉进坑里。
空间意识
在第一人称射击游戏中,这种情况很常见。使命召唤和守望先锋以几种方式利用这一点。首先,你需要对屏幕上的敌人有空间意识。你需要能够将光标放置在屏幕上他们的位置来射击他们。其次,你需要对整个地图有空间意识。如果你对地图没有空间意识,你很容易被突然袭击。这也是平台游戏的核心。在 2D 空间中理解你的位置并能够灵活操作是任何动作平台游戏中的游戏名称。Celeste 通过提供玩家每次都按预期移动的紧密控制方案充分利用了这一点。动作编排得如此之好,以至于当你犯错时,你会觉得自己是错的。
这很有趣:如果你在一个需要紧密控制的游戏中拥有松散的控制,玩家可能会感到被游戏欺骗,并可能停止游戏。这是不可取的!
收集
有没有玩过任何 CCG(可收集卡牌游戏)的玩家?这个名字就说明了这一点!可收集卡牌游戏。万智牌、炉石传说、游戏王和宝可梦只是几个例子。尽管这种机制并不显眼,但收集的概念被用于各种游戏中。
技能可以收集,同样还有武器、法典条目、盔甲,清单可以一直列下去。人类喜欢收集东西。可能你想要收集该季节中的每一张卡牌以解锁成就。这是双重收集,因为你不仅想要这些卡牌,还想要收集那些成就。可能你想要收集游戏中所有的法典,例如在《质量效应》中,游戏的背景知识来自于尽可能多地与独特事物互动,你的日志更新法典,其中包含关于独特物品、角色、种族、历史等信息。
研究
研究是进行调查以建立事实和规则的能力。我们可以以几种独特的方式使用研究的概念。一个想法是玩家应该是进行研究的那个,而不是角色。这意味着玩家看到的是角色的环境。正因为如此,玩家可以了解角色可能不知道的主题和事物。我们作为设计师可以利用这些知识,并通过概述可交互对象或使可攀爬的边缘具有特定颜色来更容易地向玩家传达信息。
另一方面,研究的概念可能指的是角色本身。游戏中的角色通过研究,在自己的世界中得出新的结论,并在身体和精神上变得更强大,同时扩展他们的意识。这看起来可能类似于收集和资源管理;然而,如果它涉及到从角色到玩家的知识转移,或者事物本质上是通过游戏学到的,那么它应该被视为研究。
限制
压力使钻石。与其被视为一种独立的机制,限制可以被视为其他/所有机制的修饰符,但我们喜欢将其作为一个独立的机制单独列出,因为并非每个交互都需要有严格的限制。可以有影响整体游戏的整体限制。例如,给整个游戏添加计时器就是一个限制。另一个例子是玩家只有 3 次生命,游戏会结束。在一个卡牌游戏中,你可能看到对牌库有硬性上限。这限制了可以进行的回合数。
当你花时间去弄清楚这些机制如何组合在一起以构建交互和体验时,你就已经设计了一个完整的机制。所有这些部件如何组合在一起是机制和交互设计的核心。让我们花点时间来探讨一些细微差别。
设计与实现
在良好的设计风格中,我们需要分解我们将要使用的力学和交互的原因。一般来说,你希望在游戏中尽量减少力学数量,同时将它们的应用扩展到许多独特的交互中。Mega Man 是一个很好的例子,展示了如何以优雅的方式使用最少的力学来实现细微的变化。移动、跳跃和射击是你唯一需要担心的事情。在击败敌人后,你将获得不同的射击能力或技能,但你仍然会使用相同的按钮来激活射击机制。这个机制一直保持为单个按钮按下,直到 Mega Man 4;当角色能够充电他的武器并且按钮分配改变以适应技能变化时。
这是一个有趣的想法:游戏玩法只涉及对机制非常有限的变化,只是改变图形和叙事。当你开始设计游戏这部分时,考虑玩家应该采取的最小行动以推进游戏,并将其分解为其最小的组成部分。
如果你正在考虑设计一个以战斗为主的游戏,你需要问自己一些问题:
-
它是哪种类型的战斗风格?
-
战斗风格是否与周围环境的主题历史相符?
-
战斗风格是否与角色相符或与其道德观形成对比?
所有上述问题的答案如何与你要让玩家感受到的情感体验相一致?这些问题绝不是详尽的。每个问题都应该进一步阐明游戏机制,并塑造你希望为玩家提供的体验。
很容易在制作的游戏中陷入舒适区的陷阱,只是沿着你正在开发的类型的其他游戏走。如果你发现自己正在设计某物,并想,“这就是一直以来的做法”,你需要评估这种交互。第一人称射击游戏(FPS)是这一点的绝佳例子,因为第一人称视角的限制。
在 FPS 领域有一个非常独特的例外,那就是《半条命》。Valve 创造了一个基于物理谜题机制的 FPS 游戏,并高度重视叙事。与之前 FPS 游戏以跑酷和超破坏为主的特点相比,这非常独特。
由于我们正在从设计角度讨论交互和机制,我们需要谈谈《Undertale》这款游戏。《Undertale》是一款以低图形保真度的角色扮演游戏开始的。游戏玩法叙事一开始感觉正常;然后战斗发生了!你很快就会学会你需要赢得战斗的战斗机制,战斗游戏感觉很好。然而,伤害事物并不总是你想要关注的。游戏颠覆了玩家的期望,其中游戏看到你要求角色伤害角色可能在游戏中与角色有情感联系的人。这种情感差异被引入视野,以展示标准的使用方式,从而颠覆了玩家的期望。这只有在设计师你研究了并深入了解游戏设计的情况下才可行。
整个章节可以很容易地用来讨论其他游戏机制和交互的设计。与其分析大量游戏,不如让我们处理我们自己的项目,并调查一些简单的机制和交互的例子,这些例子可以用于其中。
在各个部分中,我们还将探讨各种游戏设计的实现。从比喻的角度来说,我们在这里的希望是,我们将从上到下分解交互中的谜题块,以展示开发游戏就是关于在保持整体图景的同时分解每一块。
在阅读这些部分时的一些建议是,这些游戏交互的实现并非一成不变,也不是使用这些交互的唯一方式。它们只是我们方法的一个例子。试着想象你可能会如何改变每一块的设计。
我们的项目
我们正在构建一个 3D 解谜冒险游戏。我们的初始机制将使用研究作为主要组成部分。在本书的后面,在第七章“刚体与物理交互”中,关于我们对心灵感应的设计,我们将在其基础上叠加空间感知。有了这个理解,我们将构建我们的游戏循环。为了使体验值得一玩,我们可以将游戏循环定义为以下内容:
-
在环境中寻找线索
-
从线索中解决谜题
在定义了游戏循环并理解我们专注于研究作为主要机制之后,我们现在需要构建交互来创造体验。
为了开始我们的交互,我们将通过几个简单的非物理聚焦的动作进行工作。角色 Myvari 需要能够与环境交互以完成谜题并进入区域以到达她的目的地。从主题上讲,环境是她种族过去的废墟。通过我们的演示垂直切片,Myvari 将遇到多个环境谜题,她需要观察周围环境并克服障碍。这款游戏的玩家,通过控制器引导 Myvari,需要关注环境的细节并学习如何解决环境谜题。角色将面临的第一交互是楼梯。让我们深入设计这个楼梯,以真正了解其他交互在定义方面需要什么。
楼梯
在这个演示级别中,环境中存在一组供角色穿越的楼梯。理解这些楼梯向玩家传达的信息有助于建立早期交互能力的基础感,这意味着你的环境将成为玩家导航级别的首要指导因素。让我们一起来设计这个初始交互,因为这是玩家真正能够参与的第一体验。
设计
当玩家进入游戏时,Myvari 将从树林中进入一个看似正常的洞穴。进入洞穴后,你会进入一个通往一个陡峭斜坡的小走廊,这个斜坡太陡峭以至于无法爬上去。在这个斜坡的两侧各有一个池塘。每侧都有一个杠杆。所有需要发生的事情就是每个层级都需要进行交互。图 6.2显示了这种流程的预览。

图 6.2:初始交互,楼梯概述
在第一次开阔空间遭遇中,你可以感受到一种惊奇。这里有一个有手工特征的洞穴。在远处,有一个门和通往它的路径的轮廓。当你朝着门走去时,你会注意到前面的路径变得非常陡峭。四处走动,你研究这个区域,并找到杠杆。这些杠杆将楼梯带到路径上,这样你就可以爬到被看起来像是废墟所包围的门那里。
这里需要有一个简单的环境设计,包括“灯光聚集”。这是当你向一个区域添加灯光以吸引玩家注意的时候。我们倾向于走向光线更充足的地方。因此,我们需要玩家与场景中的特定模型进行交互。为了使玩家注意到这些指定的模型,你需要在这些模型上添加玩家可及性。例如,当你足够接近杠杆时,它们会略微突出。突然,一个工具提示会显示出来,告诉你需要按哪个按钮来与之交互。
与两个杠杆交互会产生点击声。从你的当前摄像机移动开,会播放一个小型电影,展示楼梯升起并就位。从这一点开始,你可以沿着楼梯移动到 Rings 区域。Rings 将是初始的谜题,其中包含环境研究。我们有一种感觉,知道应该如何进行,但实施总是给我们带来一些问题。我们需要将事物付诸实践,看看设计是否可行。让我们进入 Unity 看看感觉如何!
实现
首先,我们需要一些可以与之交互的东西。让我们稍微分析一下。这个实现有三个要点。我们需要一个交互块,一个楼梯阻挡器,以及一个管理这两个元素的管理器。
交互块
我们知道我们有两个交互点,这两个点都需要交互以满足楼梯的成功。这意味着我们应该构建一个可以多次使用的触发器。这个立方体需要有一个盒子碰撞器,因为我们将会使用碰撞来帮助设置状态。
现在,我们将查看一些代码。与第四章,角色一样,我们不会逐行检查每一行代码——只有在我们之前没有检查过或者由于某种原因对之前解释过的代码进行了更改时,我们才会仔细查看代码。我们正在查看InteractionTrigger.cs,它可以在项目的Assets/Scripts文件夹中找到。如果您还没有设置 GitHub,请参阅本书的开头部分,了解如何设置 GitHub 的说明。在第一次实现某事时,可能有一些关键区域您无法设计,因此通过一些视觉调试代码来工作是一个好主意,这样可以使您更容易实现。我们想要实现一个简单的立方体,当您进入它时,您可以与之交互。通过使用一些颜色来识别不清的措辞和注意到我们与之交互的方式。
public Color idleColor = new Color(1f, 0f, 0f, 0.5f);
public Color occupiedColor = new Color(1f, 1f, 0f, 0.5f);
public Color interactColor = new Color(0f, 1f, 0f, 0.5f);
我们在开头定义这些,以便稍后定义状态时可以引用它们。我们使用来自我们在第四章,角色中定义的输入系统的输入动作interact。在这种情况下,我们需要注意这个输入,所以我们将其放在Update上。
void Update()
{
interactPressed = interactInput.action.ReadValue<float>() > 0f;
}
这里的输入是 0 或 1,但我们要将其用作布尔值。这允许我们在想要改变状态时进行简单的if检查。为此,我们检查值是否大于 0。如果分配的交互按钮正在按下,则值为 1,这将interactPressed设置为true;否则,它被设置为false。
在接下来的几节中,我们将使用一些我们还没有讨论过的MonoBehaviour方法。这些是OnTriggerEnter、OnTriggerStay和OnTriggerLeave。正如名称所暗示的,这些方法在处理进入、停留或离开碰撞盒的碰撞状态时非常有用。
我们将从OnTriggerEnter开始。我们只使用这个来设置盒子的颜色,这样我们就可以看到我们已经进入了它。这从机械上讲没有用处,但从视觉上是有帮助的。也许在后续的抛光阶段,我们可能想要生成一些粒子或改变一些灯光,以向玩家显示他们处于可以交互的区域。现在,让我们只改变立方体材质的颜色来进行视觉调试。
void OnTriggerEnter(Collider other)
{
MyvariThirdPersonMovement player = other.GetComponent<MyvariThirdPersonMovement>();
if (player != null)
{
mat.SetColor("_BaseColor", occupiedColor);
}
}
这里发生的情况是,当玩家与盒子的碰撞盒发生碰撞时,我们正在查看是否碰撞的另一个组件具有MyvariThirdPersonMovement脚本。由于没有其他可以碰撞的项目应该具有该组件,这是一个很好的检查。我们将它分配给player变量,然后进行一个小检查,询问player值是否不是null,然后将颜色更改为占用颜色。现在我们需要处理OnTriggerStay,这将是我们允许玩家与之前碰撞过的对象交互的地方。
void OnTriggerStay(Collider other)
{
MyvariThirdPersonMovement player = other.
GetComponent<MyvariThirdPersonMovement>();
if (player != null)
{
if (interactPressed)
{
mat.SetColor("_BaseColor", interactColor);
OnInteract?.Invoke();
Debug.Log($"Interacted with {gameObject.name}");
if (disableOnInteract)
{
this.enabled = false;
this.GetComponent<BoxCollider>().enabled = false;
}
}
}
}
在到达等待交互按钮被按下的if块之前,所有这些都应该看起来与进入触发器相似。当按下交互按钮时,我们会做几件事:
-
将颜色设置为交互颜色
-
调用一个动作
-
登出以进行另一个调试检查
-
禁用它,这样我们就不可以再次与之交互
我们之前已经设置了一种颜色,所以这里应该看起来很熟悉。我们在这里使用的是交互颜色,这也很有道理!
下一个部分是调用一个动作。这有两个部分的解释。我们的管理器将监听动作被调用的信号。当我们到达管理器时,我们将全面介绍这是如何工作的。现在,理解另一个项目将等待一个信号来完全执行动作。
我们将调试设置为控制台,这样我们就可以看到逻辑中发生了什么。当我们移除调试颜色时,控制台调试将成为我们未来遇到 bug 时的指南。
最后的部分是禁用它,这样我们就不可以再次与之交互。我们需要禁用对象和碰撞器。我们这样做是因为这种交互只需要在每一侧按一次。
就这样!在我们进入管理器之前,我们需要检查楼梯阻挡器。
楼梯阻挡器
我们知道这将阻止楼梯的进一步效果,但我们已经完成了这种阻挡机制的外观。目前,它是一个带有碰撞器的调试红色方块。这不是问题,因为我们知道我们想要体验如何进行,所以我们需要制作一个阻挡器,它不允许玩家通过到达楼梯。我们将在第十二章,最终润色中添加这个视觉部分。这可能表现为楼梯是平的,玩家无法走上它们,我们使其看起来很滑,或者可能有一个岩石阻挡楼梯,在正确交互后会消失。
这里不需要编写脚本。我们将把任何逻辑都卸载到管理器上。我们需要这个管理器的一个原因是,我们不能在楼梯本身上放置脚本来自动开启或关闭。如果你有一个禁用的 GameObject,脚本将无法在没有一些外部对象(该对象具有对禁用 GameObject 的引用)启用它的前提下被激活。所以我们需要通过管理器来做这件事。现在让我们来做吧。
交互管理器
如果你在某种管理器中为交互项有一个父对象,那么将脚本拼接在一起会容易得多。在编辑器中,这通常是通过创建一个父预制体来完成的,该预制体包含一个脚本以保持交互的状态。我们在这里所做的确保楼梯在没有两个按钮都按下时不能被打开。如果没有 GameObject 知道每个项目的状态,这将很难完成。进入代码,我们定义我们的公共变量和类变量,就像我们通常做的那样设置,然后我们进入我们之前在 交互块 部分讨论的事件的第二部分。在 Awake 和 OnDestroy 部分中,我们需要处理事件监听。
void Awake()
{
leftTrigger.OnInteract.AddListener(OnLeftTriggerInteract);
rightTrigger.OnInteract.AddListener(OnRightTriggerInteract);
}
void OnDestroy()
{
leftTrigger.OnInteract.RemoveListener(OnLeftTriggerInteract);
rightTrigger.OnInteract.RemoveListener(OnRightTriggerInteract);
}
我们公开定义了每个触发器,并且它们都有自己的事件。在 Awake 中,我们监听 OnInteract 事件,如果它被调用,我们将运行作为监听器参数的函数。在这种情况下,对于左侧是 OnLeftTriggerInteract。右侧的类似命名。我们只将详细查看左侧,因为右侧非常相似。
void OnLeftTriggerInteract()
{
leftTriggerFired = true;
if (rightTriggerFired)
{
stairsRaised = true;
OnStairsRaised?.Invoke();
stairsBlocker.SetActive(false);
Debug.Log("RAISE STAIRS HERE");
}
}
如果左侧被触发,我们立即将 leftTriggerFired 设置为 true。这会检查右侧是否已经被触发。如果没有,则不会发生任何事情。如果已经触发,那么我们将 stairsRaised 设置为 true,调用另一个动作,将楼梯阻隔的 GameObject 设置为非活动状态,并记录一个字符串以帮助后续调试。
OnStairsRaised UnityAction 将会被触发,但目前还没有任何东西附加到这个动作上。在我们完成这个区域并确定确切需要什么之后,我们将添加更多到这个动作中。
有趣的是,这个设置允许玩家从左侧或右侧开始而不会出现问题。它也为我们未来的开发奠定了基础。我们不需要把所有东西都安排好,但我们确实需要理解总体想法,这样我们才能相应地编写架构。
这完成了当前楼梯谜题的实现。现在 Myvari 已经上了楼梯,我们需要解决我们的第一个主要谜题,即环状谜题。
环状物
通过楼梯,我们现在面对一扇门和环状物。
这扇门象征着对谜题的第一个以叙事驱动的答案,而不是通过灯光引导它们到一个区域。只有当你注意门上的图像并将其与谜题环相关联时,才能解开这个谜题。让我们分析一下环状谜题的设计。
设计
玩家需要解决的第一个谜题是环状谜题。当你进入谜题的平台时,你的项链会在你面前动画化,项链和中间的柱子会先发出柔和的蓝色光芒,然后逐渐消失。在门上,将有一个时间久远的铭文,描述如果正确操作,柱子应该是什么样子。
玩家需要做的是推动环中的柱子,以匹配门上找到的星体图像。这允许在小场景中进行多层次的科研和互动。玩家已经从之前给出的提示中知道环境中存在互动,一个小轮廓将指示按下按钮与柱子互动。新的信息收集将是门上图像的形状以适应地面的形状。图 6.3展示了该区域的概念。后面的一个大空白区域是门。柱子位于中间柱子外的圆圈中。总共有三个环。

图 6.3:环与环境研究谜题
解决这个谜题将打开门,但时间对这个门或周围废墟区域并不友好。当尝试打开时,走廊中有碎片会倒塌,进一步通向洞穴。我们将利用这个机会解决另一个简单的互动,即穿越狭窄的空间。
实现
我们思考了很多如何组合这个项目。每个环两侧各有一个柱子。总共有三个环。我们需要一个特定的配置来结束,这将符合我们的星座设计理念。这个问题还有一个是如何处理 Myvari 移动这些物体。起初,我们考虑了推和拉,但为了简化,我们选择了只推。这允许我们只关注一个方向的旋转以及切割动画。Myvari 不是一个大角色,拉可能没有太多意义。我们需要想出两个脚本。第一个脚本将类似于我们之前一起工作的视觉体积脚本。我们将使用这个来告诉 Myvari 在柱子的哪一侧。这告诉我们旋转的方向。在我们将其旋转到正确的位置后,我们需要有一个谜题管理器来知道最初放置柱子的位置,胜利旋转值看起来像什么,以及如何处理谜题的结束。让我们先看看简单的那个,并查看谜题触发体积。
谜题触发器
这个项目很简单。我们需要一个盒子,我们将为调试而改变它的颜色,就像之前为楼梯所做的那样,然后我们需要在游戏开始前有一些选择,这些选择是检查器中的属性。这些选择(外圈、中圈和内圈)将是它们所在的环以及它们应该移动的方向。方向是逆时针或顺时针。
尽管我们之前已经见过,但在这种颜色变化实现中,我们做了一些不同的事情,涉及到谜题访问这个类的方法。
public void SetColor(Color color)
{
meshRenderer.material.color = color;
}
这里需要注意的一个细节是公共访问修饰符。它接受一个颜色。记住这一点,当我们接下来讨论谜题脚本的管理器时。接下来,定义了两个枚举。我们将它们都放在下面:FirstPuzzleTriggerType和FirstPuzzleTriggerDirection。
public enum FirstPuzzleTriggerType
{
Outer = 0,
Middle,
Inner
}
public enum FirstPuzzleTriggerDirection
{
Clockwise = 0,
CounterClockwise
}
我们在这个类的顶部部分公开了枚举,并且在这里定义了它们。这些定义将允许我们为每个触发器选择环和方向。请看下面的图 6.4,以了解枚举在检查器中的样子。

图 6.4:检查器中公共枚举的显示
如果你选择其中任何一个,它们都会显示上面代码中看到的选择项。代码中的一个额外细节是枚举中的第一个值,我们将其赋值为 0。这将是默认行为;然而,明确地这样做可能是一个好习惯。当有人查看这段代码时,他们可以确信枚举值将从 0 开始。
谜题碎片
打开位于scripts文件夹中并附加到层次结构中FirstPuzzle游戏对象的FirstPuzzle.cs文件。我们像往常一样开始,定义我们想要使用的变量。对于这个谜题管理器,它需要有一个对支柱部分每个变换的引用,中心支柱,它负责完成谜题,以及谜题的时间属性。在我们在检查器中分配的公共变量之后,我们有一些不是公共的变量,但它们在类的逻辑中被分配和使用。花几分钟时间阅读它们的注释。我们将在本节的其余部分引用这些类变量。
虽然我们已经见过几次,但这比我们之前看到的定义更大。我们将引入整个初始化过程,并逐一讲解。
void Start()
{
// Cache references to the trigger volumes and the player
triggers = GetComponentsInChildren<FirstPuzzleTrigger>();
playerController = FindObjectOfType<CharacterController>();
// Random starting positions
outerPillars.eulerAngles = new Vector3(0f, Random.Range(-180f, 180f), 0f);
middlePillars.eulerAngles = new Vector3(0f, Random.Range(-180f, 180f), 0f);
innerPillars.eulerAngles = new Vector3(0f, Random.Range(-180f, 180f), 0f);
// Starting center spire position
centerSpire.position = new Vector3(centerSpire.position.x, centerSpireStartHeight, centerSpire.position.z);
}
关于我们继承的MonoBehaviour类,我们在游戏开始时使用Start方法初始化支柱的缓存引用和起始位置。首先,我们需要缓存每个触发器体积的引用。我们使用一个UnityEngine.Component方法,这是由于我们在文件顶部有using UnityEngine;指令。这个方法是GetComponentsInChildren<FirstPuzzleTrigger>();。在这个方法中使用的是一种称为泛型类型。你可以在代码中将任何类型放在FirstPuzzleTrigger的位置。在上面的代码中,这可以是Image或Transform。在这种情况下,我们只想获取每个触发器。我们将在稍后解释为什么我们需要以这种方式获取它们。只需知道它们都在一个桶里,等待被调用。
接下来,我们需要使用FindObjectOfType,这是另一个UnityEngine方法,但它位于Object类上。它是UnityEngine库的一部分,我们已经在请求访问其方法。它将找到角色控制器并将其返回到playerController变量。
接下来的三行用于设置环的旋转。我们希望它们是独特的,所以如果有人玩过游戏多次,每次都会有一点不同。
最后,我们设置了拼图的位置。我们使用这一行来设置中心尖塔的高度。拼图完成后,中心尖塔将升起以便交互。这将带你进入下一个部分。我们希望它在完成拼图时动画化,以揭示前进的道路。
现在,我们将继续到Update方法。这同样来自MonoBehaviour。这个拼图的有趣之处在于有很多地方我们并没有做什么。我们主要只需要等待角色将支柱移动到正确的位置。我们运行更新部分的方式就像水道中的锁系统。你必须完成第一步才能进入下一步。我们为这个系统有一个非常简化的逻辑流程。你可以在图 6.5中看到它。

图 6.5:拼图管理器的基本锁流程
在我们完成拼图管理器时,让我们记住图 6.5。这里的第一个步骤是检查胜利。让我们深入到这个流程块。胜利依赖于三个支柱足够紧密地与期望的旋转值对齐。
outerAligned = CheckAlignment(outerPillars, correctRotationOuter);
我们每帧都在检查正确的对齐。因为我们正在检查三个独立项目的对齐,所以我们不应该在所有三个环上写代码。让我们写一次,然后让它为每个支柱调用一个方法。这被称为重构。进一步挖掘,我们应该分解它是如何检查对齐的。
bool CheckAlignment(Transform pillarGroup, float correctRotation)
{
return Mathf.Abs(pillarGroup.eulerAngles.y - correctRotation) < correctThreshold;
}
首先,我们需要它返回一个布尔值。当你想要对响应运行条件时,这非常有帮助。我们要求当前的支柱和正确的旋转值。我们查看当前旋转在y值上的绝对值减去正确的旋转值。我们取这个值并检查它是否小于我们允许的“接近”阈值。如果是,那么outerAligned将返回true。如果所有三个支柱都返回true,那么CheckForVictory将返回true,这允许我们进入锁的下一个块。
下一个块是显示胜利。这似乎是一个无辜的块,只是用于调试的显示;然而,这里有一小段逻辑帮助我们处理结束块。
victoryStartTime = Time.time;
outerStartVictory = outerPillars.eulerAngles;
middleStartVictory = middlePillars.eulerAngles;
innerStartVictory = innerPillars.eulerAngles;
这四个设置的值很重要。在继续到下一个块之前,我们需要设置这些值。我们可能已经在最后一个块中完成了这个操作;然而,有时在锁中设置每一点逻辑是一个好主意,这样你可以轻松调试,并确切地知道你在逻辑中的位置以及在那个确切点你需要的数据。为了完成最后一个块,我们知道我们需要记录柱子的当前信息。我们对柱子的放置位置有一定的容忍度,所以这并不意味着谜题的解决方案总是相同的。现在我们已经存储了柱子的值并在控制台显示胜利信息以供调试,我们可以继续到最后一个块。
最后的块位于 PerformVictoryLerp 方法中。花点时间仔细查看整个方法。我们下面会分解一个单独的 Lerp。有趣的是,这个方法主要只是为最终作品动画化一些环境物品,并允许我们完成这个块,所以我们在这个谜题中不再检查旋转。
outerPillars.eulerAngles = Vector3.Lerp(outerStartVictory, outerEndVictory, lerpVal);
我们在具有使用 Slerp 方法的角色中看到过类似的情况。那个更适合球面需求。Lerp 是线性插值。你在一个时间段内将一个值转换到另一个相同类型的值。在这种情况下,它是柱子的旋转值到预定的最终胜利旋转值,因为我们对每个部分都给予了一小部分余量。处理这个方法可能会让你感到有些困难。如果你感到不知所措,只需查看一行,并慢慢地处理它。每一行都有一个任务,它提供了 Lerp 时间或在该时间段内将自身插值到另一个值的上下文。
在最后,我们还有一个位于锁系统之外的 PrintDebug 方法。这允许我们在任何给定时刻检查谜题中正在发生的事情。花点时间研究这个方法,并推测它可能会显示什么,然后运行游戏以查看你的假设是否正确。控制台是否打印出了你意料之外的内容?尝试通过跟随游戏的逻辑并识别你看到控制台消息到达的时刻,在代码中找到它。
可能会出现的下一个问题是,“这是一个很好的方法,但我们如何实际上控制它?还有,为什么我们没有涵盖 RotatePillar 方法?”这些问题都很棒!让我们在下一节中探讨它们。
谜题控制
我们是否应该将控制权放在谜题触发体积上?我们的想法是所有控制机制都应该放在包含控制的对象上。我们在 scripts 文件夹中创建了一个名为 FirstPuzzleControl.cs 的另一个脚本,它将被附加到 Character GameObjects 上。这个脚本负责设置触发体积的颜色以及从 FirstPuzzle 类中调用旋转。我们以这种方式编写它,因为我们想确保谜题的管理员会监督每个环的旋转。即使角色是使用输入启动 RotatePillar 方法的对象,谜题管理员也需要旋转玩家正在交互的任何柱子部分,因为它拥有那些 GameObjects。这样思考有点独特。试着想象一个拥有 GameObjects 并告诉它们做什么的管理员。我们已经在脚本中引用了它们;我们应该将它们保留在这个脚本中。另一种选择是将它们也引用到角色上的控制脚本中,然后你就有多个引用,可能会引起你无法看到的错误。尽量将 GameObjects 集中在单个脚本中,尽可能多地管理它们。
RotatePillar 方法稍微有些棘手。在这个方法中,我们不仅需要旋转环,还需要将角色与其一起推动。我们是如何做到这一点的呢?让我们来看看。
public void RotatePillar(FirstPuzzleTrigger trigger)
{
// Rotate pillars
float rot = (trigger.triggerDirection == FirstPuzzleTriggerDirection.Clockwise ? pushSpeed : -pushSpeed) * Time.deltaTime;
trigger.transform.parent.parent.Rotate(Vector3.up, rot);
// Keep player locked in trigger volume, facing the pillar. We need to disable the CharacterController here
// when setting a new position, otherwise it will overwrite the new
position with the player's current position
playerController.enabled = false;
float origY = playerController.transform.position.y;
playerController.transform.position = new Vector3(trigger.transform.position.x, origY, trigger.transform.position.z);
playerController.transform.forward = trigger.transform.forward;
playerController.enabled = true;
}
我们首先需要知道我们将旋转柱子 GameObjects 多远。我们将在方法的范围内将旋转角度分配给一个变量。我们将使用三元运算符来判断是顺时针还是逆时针旋转,并将其乘以 deltatime 来处理帧率变化。然后我们将使用 parent.parent.Rotate 沿其 up 向量旋转项目。旋转角度和方向在上面的行中定义,称为 rot。
一个问题是角色需要与它们交互的柱子一起移动。第二个问题是柱子会旋转,因此我们需要将角色面向她推动的柱子。为此,我们将关闭玩家的移动能力,然后直接将角色移动到触发器的位置,并在按下交互按钮时保持她在那里。我们还将使用触发体积的 forward vector 将角色指向柱子。最后,我们将控制权交还给玩家。这使得角色不会永远卡在推动柱子。
就这样!我们制作了第一个谜题。解决这个谜题之后会发生什么?门试图打开,但稍微断裂,只有一个小空间可以移动。让我们花点时间分析一下这可能会很有用。
狭窄的空间
有时候,你的游戏需要加载下一个场景,但你可能不希望加载屏幕将你的玩家从沉浸感中拉出来,或者你可能只是想增加一些环境紧张感。可能你想要两者兼得!狭窄的空间是达到这些情况的一个常用工具。让我们来看看我们如何在我们的工作中使用狭窄空间。
设计
狭窄空间的概念是一个有趣的设计用途。我们以两种方式使用它。第一种方式是增加探索和移动的紧张感。你有一个角色必须穿过一个非常狭窄的空间,她刚刚看到它塌陷到位。
第二点是,这是一个常用的设计,用于过渡。由于我们只在这个游戏的小垂直部分上使用这个设计概念,而且我们不需要加载地图的多个部分,因此从这个原因来看,它不是必需的,但它对你这个有抱负的设计师来说是一个很好的教学点。
这有助于为玩家设定期望,让他们知道将会有缓慢移动的部分,动画紧密,摄像机靠近玩家以增加紧张感。当这种较长的动画和动作发生时,系统将有时间将下一个区域加载到内存中,而无需加载屏幕。这个技巧很棒,因为它不会破坏沉浸感,同时允许保留细节。没有什么比在你面前看到物体加载成实体更能打破你的信念了。在你完成穿越封闭空间的工作后,岩石会自然落下,封闭通道。这不仅阻止了任何后退动作,还给人一种向前移动的必要性。
实现
这种实现的初始版本很简单。我们将让 Cinemachine 摄像机穿过空间,以获得电影所需的时机感,同时不允许玩家有任何输入。我们通过以下代码进行这种初始实现:
Void SetPlayerEnabled(bool enabled)
{
var cams = playerRoot.GetComponentsInChildren<CinemachineVirtualCamera>(true);
foreach(var cam in cams)
{
cam.gameObject.SetActive(enable);
}
playerRoot.GetComponentInChildren<MyvariThirdPersonMovement>().enabled = enable;
}
我们需要在子对象中找到虚拟摄像机,并在启用它们的同时禁用玩家角色。在第十二章,最终润色中,当我们触发电影时,我们将调用此代码。但我们将调用为电影制作的摄像机,而不是子对象中的虚拟摄像机。
这种实现非常适合设置逻辑,无需担心每个电影的细微差别,这非常耗时和动画密集。
交互式体积
这是机械领域的瑞士军刀。交互式体积的用途如此之多,我们无法在这本书中全部涵盖。这也并不合理,因为定义它会削弱可能设计的创造力。这不是一个应该在高粒度上详细说明的工具。相反,让我们来看看我们将如何使用它,以及一些关于它的总体想法。
设计
由于这是一个冒险解谜游戏,会有一些需要体积的地方,当角色进入时,会发生某些事情。这个定义是故意很宽泛的。我们也在使用 Cinemachine 作为我们角色的主摄像机。这允许我们在触发体积时在某些地方连接虚拟摄像机。以下是一些我们可以使用交互式体积执行的示例:
-
将摄像机移过悬崖,以产生更高的焦虑感
-
触发岩石坠落
-
当你穿过水时,改变行走动画以变得更慢
-
改变环境中的照明
-
生成 GameObjects
这个列表绝不是详尽的,因为体积是一种创意工具,允许进行交互。我们只以几种方式使用它们,但可能性是无限的。在设计时,让您的想象力自由驰骋,使用交互式体积。
对于许多游戏来说,这很强大,尤其是对于我们来说,因为我们有以环境驱动、研究为重点的机制。我们的交互需要环境向玩家解释正在发生的事情以及如何前进。在下面的脚本部分,我们将逐一介绍本章的每个交互式体积。你可以期待在未来的章节中看到更多体积的使用,尤其是在抛光阶段,因为我们可以通过许多小的交互来增强体验。这将有助于使环境和游戏玩法更具沉浸感。
实现
幸运的是,你已经看到了交互式体积的两个版本。回顾一下我们之前查看的实现,并仔细注意体积。它们在用途上独特,可以教你一些关于在没有所有艺术资源完成的情况下开发环境的宝贵经验。
考虑到我们到目前为止可能使用交互式体积的其他一些方法可能是个好主意。你有没有想要添加的体积?
我们为什么不简要回顾一下我们在游戏中使用它们的地方?
-
我们在可以使用输入动作的任何地方使用交互式体积。一个例子可能是用于访问楼梯的楼梯按钮。
-
我们添加了一个体积,以便知道玩家位于第一个拼图的区域内。这允许摄像机移动到一个更有利的位置,以便从视觉上理解拼图。
-
在拼图块上的触发器会让你知道你足够接近可以与之交互。
-
有一个体积可以告诉你已经进入了进入狭窄空间的触发点。
-
过桥时,有一个小体积可以改变摄像机角度,以获得更电影化的镜头。
-
在桥的尽头,有一个体积可以触发另一个狭窄空间的 cinematics。
-
当你在悬崖上时,有一个触发器会释放一块巨石落在你身上,这时你会举起手臂进行防御。这将触发你发现一种新的力量,这是一种新的机制。
-
更多触发器用于打开另一扇门。
-
在你可以使用心灵感应能力与之交互的物品上设置了触发器。
-
在最终谜题碎片上设置了触发器。
这是对我们核心游戏玩法中所有触发器的总结。还有一些其他触发器与周围环境和生物有关,但它们只是简单的碰撞触发器,负责引起小的变化或鸟类或鹿的简单移动。它们位于随机位置,用于美观目的。
摘要
在这一章中,我们讨论了交互和机制的设计与实现。尽管玩家的体验和交互看起来相当简单,但玩家便利性的设计深度使得玩家能够了解自己的限制并导航游戏玩法。我们花了大量时间讨论交互和机制。我们定义了游戏循环并分解了机制工具箱的部分。这是一次非常快速而简短的关于各种游戏体验的介绍。最后,我们分解了我们的一些游戏。
我们分析了楼梯交互及其管理方式。我们还讨论了楼梯问题存在的原因以及解决方案需要发生的地方。然后,在完成这些工作之后,我们讨论了第一个谜题的设计。在详细解释之后,我们分解了我们的实现版本。一旦这个谜题完成,接下来是一个紧凑的空间段,如果我们是一个更大规模的项目,可以用来加载剩余的关卡。最后,有一小节介绍了如何使用交互体积。由于我们在之前的实现中使用了两种不同的交互体积,我们也对它们进行了讨论。
总体来说,这一章信息量很大。请在这里稍作停顿,消化一下你刚刚学到的内容。即使你觉得可以继续前进,也让我们放松一下,让大脑处理所有信息。在下一章中,我们将讨论物理力学和交互。
第七章:刚体与物理交互
在许多游戏交互中,需要物理。无论你有物品下落、弹跳,还是以程序方式对碰撞做出反应,你很可能需要在你的 GameObject 上使用 Rigidbody 组件。此组件与物理一起工作。我们将首先介绍 Rigidbody 组件的几个用例。一旦我们了解了这些,我们将花些时间解释我们如何在项目中使用物理进行交互。最后,我们将尽可能详细地展示用于实现这些交互的脚本。像往常一样,GitHub 上的项目文件将遵循Readme文件中的结构。本章包括以下主题:
-
Rigidbody 组件
-
碰撞检测
-
设计与实现
-
心灵感应与物理
Rigidbody 组件
这个强大的以物理为重点的组件可以添加到 GameObject 中,通过物理确定其位置。默认情况下,只需将此组件添加到 GameObject 中,就会使其运动受到重力的影响。为了了解 Unity 如何使用物理,让我们花些时间看看这个组件。
图 7.1 是 Unity 中 Rigidbody 的截图。这里有一个 Rigidbody 2D 组件。不要在 3D 应用程序中使用此组件。这个问题的主要问题是 2D 和 3D 版本的物理步骤不会相互交互。最好选择一个并坚持下去!在图之后,我们将详细介绍 Rigidbody 组件的所有部分。

图 7.1:Rigidbody 组件
质量
Rigidbody 的质量属性指的是该对象与其他对象质量的关系。这不会使重力对其产生不同的影响,但它会影响与其他对象的碰撞。例如,如果两个 GameObject 在 Rigidbody 上的质量相同,除了质量外,它们相撞时,质量较大的物品会表现得像更重。就像在现实世界中一样,质量不会使物品下落得更快。这是由于物体的阻力造成的。
阻力
带有阻力的对象将降低由于重力引起的加速度。一个例子就是降落伞。这个物体会大大降低下落的加速度。例如,跳伞者的阻力非常低,当他们打开降落伞时,阻力会大大增加。这与物体的旋转无关。
角阻力
角阻力与阻力概念相同;然而,它专门关注旋转值。如果你角阻力的值非常小,物体在被撞击或碰撞时将旋转,具体取决于碰撞物体的来角。如果你提高这个值,它将旋转得少一些。
使用重力布尔值
使用重力布尔值仅允许重力影响 GameObject;Rigidbody 是其组件。如图 7.2所示,在编辑 > 项目设置 > 物理中,重力被定义为-9.81,这与地球的重力相同。将Y轴重力设置调整为-9.81将使玩家在模拟地球重力时感到最熟悉。如果你正在制作重力较小且始终相同的游戏,你可以在这里设置它。你还可以在代码中设置重力:
Physics.Gravity = Vector3(0, 0, 0,);
0 应该替换为所需的引力值,通常在y方向。

图 7.2:项目设置 – 物理设置
是否是运动学布尔值
在设计关卡时,可能会有一些在运行时需要影响其他 Rigidbody 物理的移动项目。一个你可以想象到的非常简单的例子是一个带有 Rigidbody 的球体在大型立方体上方。当你按下播放时,球体会像预期的那样落下并击中立方体。如果你将是否运动学布尔值设置为 false 并尝试旋转立方体,球体会保持原位并穿过立方体。这是由于立方体在球体击中它并停止后没有作为移动体更新自己。设置此标志有助于优化过程,并且可以设置每个已知的静态项目,这些项目仍然需要具有 Rigidbody 组件。尽管如此,如果你需要在运行时更新物理,将地面设置为运动学,并在你旋转它时,球体将像预期的那样反应,并试图从立方体的斜坡下滚落。
这是在你开始使用物理项目时一个非常常见的错误。如果在运行时你的 Rigidbody 项目没有以你预期的方式移动,请检查它们是否应该是运动学的。
插值
插值意味着将事物放置在其他事物之间。在我们的案例中,我们需要知道插值是否试图在物理更新中实现三个参数之一。
这些参数是:
-
无:不插值或外推
-
插值:将对象放置在当前帧和下一帧之间
-
外推:假设从前一帧的下一个位置,并将其放置在你认为它可能会去的地方
确定适当的插值参数可能很复杂。原因是处理插值时有多于一个选项,因此,使得解决方案不是那么直接就能解决。
有多个变量需要考虑。这些变量可能包含这些问题,例如:摄像机是如何移动的?对象移动得快吗?你是否担心碰撞看起来是否正确?你是否担心对象每次跟随摄像机移动时都会移动不正确?一个简单的答案是,如果你的摄像机使用 Rigidbody 跟随角色,那么将其设置为插值,其他所有设置为无。
深入了解物理系统,这个系统是以固定间隔进行计算的,与图形渲染不同。游戏中图形可能会稍微滞后并突然出现,而物理计算则始终以固定间隔进行。这可能导致视觉上的伪影,例如物体剪裁到墙上。如果快速移动的物体被相机紧随其后,并且与墙壁或周围的 GameObject 发生碰撞,就会看到物体剪裁到墙内或其他游戏对象。物体最初会穿过墙壁,直到物理更新,然后它会像反弹一样更新。
在这种情况下,你可能会想选择插值选项,因为物理系统会在图形渲染时插值中间值。这不允许在物理意义上的移动时发生剪裁。但这会消耗一些性能,因为它在不同于正常情况的不同间隔计算值。
外推在预测未来值方面做得很好。这对于模拟飞行物体很有帮助,但不适用于碰撞检测,因为它会假设物体已经过了墙壁或对象,并以更高的帧率和移动进行剪裁。如果移动被紧密跟随,可以使用插值或外推。
最好从插值开始,看看它是否适合你体验中的移动。如果感觉太慢,尝试外推。权衡每个选项的优缺点,以更高的移动速度在你的动作序列中确定你需要使用哪种插值方法。
理解这一点将允许你为使用物理模拟的物品选择最佳的物理值和图形表示选项。
碰撞检测
当使用物理来确定 GameObject 的位置时,需要碰撞检查来确定你的物体是否与另一个物体发生了碰撞,无论它是静止的还是正在场景中移动。现在你已经了解到物理是固定的,而渲染不是固定的,这是一个有趣的困境。物理系统不能假设每个对象使用什么碰撞类型或插值。我们需要有几种选项,以最好地满足每个 GameObject 的物理需求。有四种不同的碰撞检测类型需要考虑:离散、连续、连续动态和连续推测。如果你有一个快速移动的 GameObject,它可能会穿过 GameObject,这意味着它将不知道它已经击中了碰撞体,并且会继续穿过它,因为物理更新正在进行。这可以通过碰撞检测模式来防止。每种模式对性能有不同的影响;然而,一般规则是,快速物体设置为连续动态,而它们可能与之碰撞的物体应该设置为动态。其他选项在下面的每个选择的分解中解释。
离散
这种碰撞检测模式是性能最好的模式,它恰当地被命名为离散,因为它只检查固定间隔的物理碰撞,如前所述。如果你有一面墙和一个盒子碰撞体,并且有一个球以足够快的速度移动,以至于在墙之前的已知位置没有与之碰撞,而下一个固定更新已经过了墙,那么就没有碰撞!一开始这可能会让人感到沮丧,因为它看起来好像不起作用,或者更令人沮丧的是,它可能只是间歇性地发生,因为球在运行模拟几次时可能已经与墙发生了碰撞。你应该理解为什么会发生这种情况,这样你就可以根据物理模拟的需要做出不同的模式选择。这种情况发生的原因是物理更新没有意识到物体应该受到任何影响。离散模式的物理循环只会检查物体是否在循环中需要改变轨迹。如果你有一个快速移动的物体,定义为每帧移动距离超过其高度或宽度的物体,那么可能存在这样一个点,这个物体已经超过了另一个物体,而物理系统并不知道要对此做出反应。
如果没有快速移动的物品,离散是一个非常好的选择。如果你计划有快速移动的物体,那么连续就是答案,但请阅读关于其他选项的其余内容,因为它们都不直观地相互作用。
连续
如果你选择连续,你可能会看到物体仍然会穿过你可能没有预料到的 GameObject。非常重要的一点是,连续碰撞检测只检查你的 GameObject 是否与静态物体发生碰撞。此模式资源消耗大,应谨慎使用。
静态物体是带有碰撞组件但没有 Rigidbody 组件的 GameObject。它们不使用物理更新。在描述碰撞检测时,将会有一些模式只能与静态 GameObject 一起使用。
使用连续模式的物体例子是快速移动的 GameObject,它们只需要与静态物品发生碰撞。这个最简单的例子是弹珠机。这是一款游戏,一个小金属球从屏幕顶部掉落并下落,撞击静态物品,然后弹开。场上的所有物品都是静态的,所以不会有穿过的现象。
连续动态
此模式与连续模式非常相似;然而,它还可以与使用 Rigidbody 组件的 GameObject 一起工作。这在游戏机制中是一种常见用法。正如你可以想象的那样,添加与 Rigidbody 组件一起工作的能力会增加游戏中的资源成本。这比标准的连续模式更耗费资源。
连续动态的一个例子是你可能玩过的游戏,Smash Hit。这是一款移动游戏,你作为玩家在轨道上向前移动。当你点击屏幕时,一个金属球会向你点击的位置射出。如果它与玻璃相撞,它会破碎。玻璃是动态的,并且在与球接触的地方进行交互。那些破碎的碎片也是动态的,并且在下落时与环境交互。如果不是动态的,球会直接穿过玻璃。这将使游戏变得非常无趣!
连续预测
“预测”一词暗示了一种猜测的感觉。系统正在预测碰撞是否会发生。
此模式执行与连续动态相同的功能,具有此设置的物体可以与静态和动态的 GameObject 发生碰撞;然而,它更节省资源。尽管如此,会有一定的精度损失。如果两个物体都设置了连续预测,那么它们可能会在没有接触的情况下相互弹开。这是因为两个物体都在预测它们在下一帧的位置,这使得它们认为它们应该相互弹开。
这的一个例子是名为 Beat Saber 的游戏。这是一款 VR 游戏,你必须以特定角度击打方块以正确切割它们。如果你的光剑检测设置为连续预测,这将允许你知道你会击中那些以高速向你移动的方块。
理解所有碰撞检测模式将帮助您为基于物理的工作创建正确的设置。花时间在自己的项目中尝试这些模式,以获得它们如何协同工作的良好感觉。
约束
现在我们已经讨论了一些难题,让我们回到一个更简单的话题:约束!这确实如您所想的那样工作。如果您的项目在特定轴上不应该移动或旋转,您可以对其进行约束。一个例子是具有移动平台的平台游戏。您希望它们移动,但可能不沿特定轴移动。为了确保平台不会偏离轨道,您可以在x、y或z方向上约束 GameObject,使其永远不会在该方向上更新。
这是在 Rigidbody 组件上可编辑字段的最后。最后一部分是用于运行时调试的只读字段。让我们看看您可以从这些字段中获得哪些信息。
信息
Rigidbody 组件的信息块对于处理物理和调试可能出现的奇怪行为至关重要。每个应用程序都可能存在独特的问题。在游戏运行时查看信息对象,可以轻松地进行调试。本节包含许多值:
-
速度:速度的大小
-
速度:Rigidbody 位置的变化率
-
角速度:以每秒弧度计量的 Rigidbody 的角速度向量
-
惯性张量:位于该物体质心参考系中并通过惯性张量旋转旋转的对角矩阵
-
惯性张量旋转:惯性张量的旋转
-
局部质心:相对于变换原点的质心
-
世界质心:Rigidbody 在全局空间中的质心
-
睡眠状态:一种优化策略,不是总是考虑每个对象,有两个设置:
-
唤醒:物理正在考虑此 Rigidbody
-
休眠:物理不再考虑此 Rigidbody
-
上述每个值都有其独特的作用,具体取决于您在运行时试图监视或调试什么。在与之前提到的平台游戏一起工作时,您可能会认为您的平台应该与您的角色对齐,但某物将其推离轨道,使其不足以让角色着陆。通过信息块,您可以监视运动或速度。如果z方向上不应该有速度,那么查看该值将让您知道它是否按预期工作。
现在我们对 Rigidbody 3D 组件的工作原理有了清晰的认识,如果构建以物理为重点的交互时遇到一些令人困惑的运动,可以参考这些页面。
设计和实现考虑因素
很容易尝试为每个 GameObject 添加物理属性以获得交互中的运动。并不是每个物品都需要 Rigidbody 来完成你的交互所需的运动方式。最终,一切都关乎每秒帧数。尽量让任何移动的物品没有 Rigidbody 组件,但如果需要,就添加它们。
心灵感应和物理交互
对于我们游戏中的第一个谜题,我们重点在于使环境叙事成为关键兴趣点。从你走进第一个房间的那一刻起,你的视线就会被放在后门上,那里藏有谜题的解决方案。在最后的谜题中,我们需要迫使玩家更多地使用脑力来解决问题,而不是在他们周围寻找答案。为此,我们决定赋予玩家 Myvari(我们的角色)意识到她一直拥有的心灵感应能力。我们有三步来让玩家达到这个理解点。
石头掉落
心灵感应在这个游戏中还没有以任何形式出现过。一些魔法来自她的项链,但我们需要提供一些信息来告诉玩家她身上有东西。电影场景对此很有效。我们需要设计交互。
设计
在完成第一个门谜题之后,你遇到了一个宽敞的大厅,里面摆放着你过去的旧雕像。这是对她种族过去文化的一个很好的了解。这里没有需要解决的问题;这里只有一段愉快的散步。在最后一个雕像后面,有一个狭窄的空间可以穿过去,通往悬崖小径。沿着小径大约走了一半,一些石头掉了下来。这触发了一个电影效果,其中 Myvari 用她的心灵感应来保护自己免受这些掉落石头的伤害。看起来很困惑,她需要继续前进以了解发生了什么。她的冒险精神在召唤她继续前进。
实现
需要在这里实现的是两件事。一部分是石头和 Myvari 的电影场景。电影场景是指用户对交互没有控制权的时候。这有助于获取知识,但不应该过度使用,因为游戏可能会变成一部互动电影。电影场景应该适度使用。第二部分是基于物理的石头作为从大石头掉落时的次要运动。
电影场景将像之前一样触发:我们关闭玩家操纵 Myvari 或摄像机的权限,并在移动摄像机的同时切换到电影场景的动画,以强调我们想要的对象,在这种情况下,是大石头。如果你需要复习这一点,请查看第六章,交互和机制,在实现狭窄空间时。
然而,基于物理的石头我们不仅仅可以动画化。我们希望它们看起来像是自己掉下来的。这使大石头看起来像是掉下来的,这有助于营造这个地点可能真实存在的沉浸感。
尽管这显示了来自 Myvari 的心灵感应,但我们仍需要让玩家执行交互,否则这只是一个他们无法使用的技能。我们将在下一节中介绍玩家的交互。
破碎的基座
这是玩家第一次使用 Myvari 新获得的力量。我们需要设计这个谜题,使其不可能错过,因为玩家不习惯使用这种力量。这个基座是最终谜题的一个小型版本。
在这个微型谜题中,你需要将掉落的碎片放置在基座上以修复它。我们需要非常小心地设计这一点,以确保在玩家触摸交互按钮之前,他们的体验能够解释这是如何工作的。让我们一起来回顾一下设计。
设计
在我们沿着悬崖路径和一座小碎裂的桥梁前进后,桥梁随后倒塌,返回的路已经不通。唯一的出路是通过一扇大门。当我们走近它时,它将开始打开,进入一个宽敞的洞穴,底部积满了水,背景中有废墟,还有通往必然毁灭的深渊。在 Myvari 正前方有一个破碎的基座,但破碎的碎片就在它附近的地面上。看着它,我们可以看到它被与保护 Myvari 免受落石伤害的相同颜色的能量勾勒出来。我们将显示一个 UI 辅助器,显示要按哪个按钮,我们将在第八章,用户界面和菜单中详细说明。这将使与她的能力交互与一个按钮相关联,为玩家提供自主权。当我们按下按钮时,Myvari 将将破碎的碎片从地上抬起并放置在基座上,碎片固定并亮起。按下交互按钮后,将场景从开放空间转变为夜景,并且从下方升起一些水,揭示通往废墟的道路。
实现
我们知道我们想要包含的机制是最终谜题的一个子集。为了做到这一点,我们不想只为这个单一的项目编写代码,所以我们设置它使用公共枚举作为简单的独立项目。
为了尽可能保持可读性,我们将要求您花时间阅读这一节,它关于最终谜题。我们将解释一些更高级的功能,所有这些功能都一直累积到结尾。我们在代码中使用了惊人的 Unity 时间控制,这将需要一些解释,并且像我们之前那样分解它将有助于您理解。因此,让我们继续到最后一个谜题的设计,然后我们将分解这个和最终谜题的所有部分实现。
最终谜题
我们已经到达了最后的重大谜题。幸运的是,我们花了时间向玩家展示 Myvari 通过来自滚石的压力获得了什么。然后我们学会了如何通过修复破碎的基座来使用它到达这里。现在这个谜题稍微开放了一些,但允许环境教会玩家他们需要做什么。让我们深入探讨,看看我们是如何设计这个最后一个谜题的。
设计
现在您已经到达了废墟,背景中有些建筑照亮了柱子上的符文。这对应于地面上连接到所有柱子的某些电缆。这个谜题由六个柱子组成,它们将电力连接到位于废墟中心的主体树,该树上有电线连接。只有三个柱子的电线连接是正确的。Myvari 需要使用她的心灵感应能力,根据地上的电线连接正确的柱子。将电力引入树中会在树中打开一个小隔间,里面放着一顶王冠。王冠将通过一段电影式画面揭示,并将结束这个垂直切片的游戏玩法。现在我们已经了解了我们需要做什么的一般想法,让我们继续到实现部分。
实现
这个谜题的实现是心灵感应机制的完成。当我们编写这个时,我们让自己深入到更高级的主题。为了确保这有意义,我们将在这里介绍所有主题,尽可能详细地分解它们。请确保您注意细节,因为这里有一些信息在最初看起来可能是隐藏的或反直觉的。
我们将要涵盖的编程主题包括:
-
执行顺序
-
静态方法
-
UnityAction(委托) -
协程
让我们先了解一下 Unity 的执行顺序。我们还没有花时间讨论它底层的工作细节。
执行顺序
当在运行时、在编辑器中播放或在构建中打开时,每一帧的执行都有一个顺序。我们本想展示一个流程图的截图,但它有点太大。相反,我们将在此处放置一个链接,以及一个 Google 搜索词,供您在线搜索以找到网站并查看此流程图。我将在这里介绍高级主题,以及它们在每个受影响部分中的重要性原因。
docs.unity3d.com/Manual/ExecutionOrder.xhtml
Google 搜索词:Unity Execution Order
这里的主要概念是必须对某些信息片段的执行顺序有一个层次结构。我们需要深入思考,以确定每一帧将处理什么。不愉快的真相是,有很多东西需要思考。以下是按时间顺序排列的列表,包含执行顺序的最高级术语,以及关于每个术语的一小段信息:
-
初始化:这仅适用于
Awake和onEnable。 -
编辑器:当添加脚本且不在播放模式时重置。
-
初始化:初始化的第二部分仅适用于
Monobehaviour方法的Start。 -
物理:所有物理更新都将发生在这里。如果固定时间步长设置高于帧更新时间,它可能每帧运行多次。
-
输入事件:任何非更新相关的输入,例如
OnMouseDown。 -
游戏逻辑: 更新、协程逻辑和 yield、动画事件、写入属性和
LateUpdate在这里运行。这些将在本章后面关于游戏逻辑的实现中更加明显。 -
场景渲染: 许多场景渲染函数在这里每帧运行,以处理从相机中剔除对象、可见对象和后渲染。我们不会对此进行深入分析,如果你好奇,请阅读执行顺序手册以获取更多信息。
-
Gizmo 渲染: 特指 Unity 编辑器的
OnDrawGizmo方法。 -
GUI 渲染:
OnGui方法,可以在每一帧运行多次。 -
帧尾: 允许在帧尾暂停或 yield 协程,等待所有其他操作完成后再从游戏逻辑部分的顶部重新开始。
-
暂停: 当应用程序被暂停时;在应用程序暂停之前,运行一个单独的帧。
-
退役: 使用
OnApplicationQuit、OnDisable和OnDestroy按此顺序清理内存。
在我们进入下一节之前,有一件事你需要确保你理解。你不必以任何方式理解前面列出的所有内容。那里有很多东西要学习,如果你查看执行顺序文档,你会看到列出的每个方法的更详细说明。我们将展示执行的部分,并在本章的其余部分解释影响我们代码的因素。
从按时间顺序列出的高级部分的关键点来看,Unity 有一个顺序。这对于开发者来说是一个令人欣慰的概念。当你对为什么事情以这种方式发生感到困惑时,你可以依赖这个顺序来查看是否是执行顺序问题,你可能正在遇到。
在接下来的几节中,我们将展示执行顺序中需要注意的部分的图像。这将允许你看到它如何被用于你未来的开发工作。
现在我们已经了解了执行顺序,我们应该进入代码部分。我们使用三个脚本来实现与心灵感应机制的工作:
-
PhysicsPuzzleTrigger.cs -
PhysicsPuzzlePiece.cs -
FinalPuzzle.cs
PhysicsPuzzleTrigger.cs中有两段重要的代码需要了解:PhysicsPuzzleTrigger类和PhysicsPuzzlePieceType枚举。我们将首先处理PhysicsPuzzlePieceType,因为它比触发器更容易理解。我们有一个枚举,允许我们在 GameObject 上选择它是哪种拼图类型。我们定义如下:
public enum PhysicsPuzzlePieceType
{
First = 0,
Second,
Third,
Intro,
Any
}
然后,在PhysicsPuzzlePiece.cs脚本中,我们按以下方式实现:
public class PhysicsPuzzlePiece : MonoBehaviour
{
public PhysicsPuzzlePieceType pieceType;
}
当我们将PhysicsPuzzlePiece.cs脚本添加到任何 GameObject 时,我们就会得到一个下拉菜单来选择它的类型。当你想要明确的项目能够配合在一起时,这非常有用。我们正在使用这个来使用相同的机制,但允许不同的拼图类型。
在上面的破碎的基石部分,我们提到会在整个机制的实施中解释它。我们做的是允许Intro选项与这个机制对齐,并明确该动作。尽管在那个位置无法获得最终的谜题碎片,但这是一种很好的实践,以确保数据与代码的一致性。
让我们回到PhysicsPuzzleTrigger.cs代码。我们首先声明了我们迄今为止习惯使用的字段,但在第 12 行,有一些独特之处,涉及两个我们需要讨论的概念。这是static和UnityAction的使用:
public static UnityAction<PhysicsPuzzleTrigger, PhysicsPuzzlePiece> OnPieceSlotted;
我们将暂时跳过描述这一行的具体做法,来解释static和UnityAction的上下文。在这样做之后,我们将继续探讨我们如何在代码中为这个机制使用它们。
静态方法
静态方法、字段、属性或事件可以在命名空间内的任何类上调用,无需使用using指令或继承。假设你有一个脚本,其中有一个字段如下所示:
public class StaticTest
{
public static int StaticInt = 10;
}
你可以在同一个项目中创建另一个脚本,无需在调用或继承该脚本时特别指定,就可以像这样访问它:
public class UseStaticTest
{
int BaseNumber = 0;
int NewNumber = BaseNumber + StaticTest.StaticInt;
}
单独来看,这可能看起来并不很有用,但这个概念是现在需要记住的重要部分。类的static成员可以通过在所需成员之前使用类名来被其他类访问。
这种用法的一个常见例子是保持某个计数,因为static字段只有一个实例。我们正在使用它来存储UnityAction。在我们深入探讨如何直接使用这些之前,我们需要先了解这一点。
UnityActions
UnityAction是 Unity 特定的委托。在 C#中,委托是一种通用的方法概念,它有一个参数列表,并返回一个特定类型。有趣的是,UnityAction默认返回void。解释委托的一个常见方式是通过订阅模型的概念。这意味着委托正在寻找要附加到其上的方法,当某个东西使用委托时,它将尝试运行附加的方法,只要这些方法返回相同的类型。这有点抽象,所以让我们看一个例子。我们将使用UnityAction MathAction来增加按钮被按下的次数,然后看看这个新数字是偶数还是奇数:
using UnityEngine.UI;
public class UnityActionTest : MonoBehaviour
{
public Button AddButton;
private UnityAction MathAction;
float TimesClicked;
void Start()
{
AddButton = GetComponent<Button>();
MathAction += AddOne;
MathAction += CheckEven;
AddButton.onClick.AddListener(MathAction);
}
void AddOne()
{
TimesClicked++;
Debug.Log("Clicked count : " + TimesClicked);
}
void CheckEven()
{
if (TimesClicked % 2 == 0)
{
Debug.Log("This click was even!");
}
else
{
Debug.Log("ThIs ClIcK WaS OdD.");
}
}
}
我们使用的是Button类,所以请确保导入UnityEngine.UI,这样我们就可以使用该类中的按钮。沿着这些行向下,我们创建了一个名为MathAction的新UnityAction。在Start方法中,我们获取了按钮,以便向其添加逻辑。然后我们将AddOne和CheckEven方法附加到UnityAction上。你看到的+=是MathAction依次将这些方法附加到其上。
加法赋值运算符——我们使用一种特殊的“语法糖”来使代码更易于阅读,并减少冗余。加法赋值运算符看起来像这样:
MathAction += AddOne
另一种说法是:
MathAction = MathAction + AddOne;
然后你看到我们将UnityAction分配给了按钮的监听器。当你按下按钮时,这两个函数都会运行,因为UnityAction被分配给了它们。
在我们进一步深入代码之前,我们需要覆盖一个更多的话题,那就是协程。
协程
协程允许你将任务分散到多个帧上。这不是多线程的一种形式。每个动作仍然在主线程上运行。协程的力量在于它们允许通过一个新的术语yield进行可暂停操作。查看下面的执行顺序图,你可能记得在游戏逻辑部分的Update之后看到了yield null。如果你没有在浏览器标签页上打开执行顺序,请查看图 7.3。左边的注释很好地说明了这一点。如果一个协程之前已经 yield 或暂停,并且它应该恢复,它将在执行顺序中的那个点恢复。

图 7.3:执行顺序的游戏逻辑
那真是太棒了,不是吗?你是怎么知道要恢复的呢?这是一个好问题,读者。它知道应该恢复是因为代码中的逻辑。Unity 文档中有一个出色的例子,介绍了使用协程从不透明到透明的基本淡入淡出。让我们快速浏览一下:
void Update()
{
if (Input.GetKeyDown("f"))
{
StartCoroutine(Fade());
}
}
IEnumerator Fade()
{
Color c = renderer.material.color;
for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
{
c.a = alpha;
renderer.material.color = c;
yield return null;
}
}
我将可能对你来说新的三件事情加粗。StartCoroutine(Fade())是请求应用程序使用Fade方法启动一个协程。你将在yield语句底部的游戏逻辑开始时启动协程;再次参考图 7.3以了解这一点。
IEnumerator表示此方法是可以迭代的。记得你上次创建方法的时候。名称前的关键字是类型。如果我们什么也不返回,我们使用void,但由于这将进行迭代,它需要知道。我们通过添加IEnumerable作为返回类型来让计算机知道这一点。
最后的部分是yield return null。第一次查看for循环时可能会觉得有点棘手。在大多数情况下,return会带你退出循环,但由于我们这里有一个yield,Unity 会询问我们是否已经完成了方法中的所有内容。它在从当前 alpha 减去 0.1f 后暂停,等待游戏逻辑部分再次开始以再次执行,直到满足for循环的逻辑。一旦完成,它就不再 yield。
总结这段代码,按下F将使此脚本所在的 GameObject 从场景中淡出。我们认为你对这些概念已经有了足够的了解。让我们回到我们项目中的代码,完成我们的实现。
返回代码
好吧……我们稍微偏离了一下,来解释一些关键概念,但现在我们回来了。让我们重新打开PhysicsPuzzleTrigger.cs。这里的理念是,你拥有心灵感应能力,当你将一个物品移到其触发体积附近时,它将在这个我们定义的过渡期间自行移动到正确的位置。我们之前已经看到了OnTriggerEnter,所以体积触发器并不令人惊讶。我们确实希望它能够自行移动,所以我们需要禁用 Rigidbody 的一些字段和禁用碰撞器。这在PhysicsPuzzleTrigger.cs的第 28-33 行完成。
现在,我们到了可以看到新代码的地方。我们需要设置从哪里到哪里的引用,因为此脚本位于几个 GameObject 上,所以我们需要引用它们的相对位置。
然后在第 40 行开始协程。
StartCoroutine(TransitionTween());
我们有一些代码用于更改触发器的颜色;这是用于调试的临时代码。
然后我们有一个tween循环,这是一个动画术语,表示“之间”,在我们的情况下意味着运动的变化。我们的while循环会持续到tweenDuration被设置为,从开始的时间进行归一化。这被定义为delta。然后我们将位置 Lerp 到我们想要的变换,将旋转 Slerp 到我们想要的变换:
while (Time.time - tweenStart < tweenDuration)
{
float delta = (Time.time - tweenStart) / tweenDuration;
tweenPiece.position = Vector3.Lerp(tweenStartPos, transform.position, delta);
tweenPiece.eulerAngles = Vector3.Slerp(tweenStartRot, transform.eulerAngles, delta);
yield return null;
}
最后,我们看到yield return null!
我们现在暂停,直到下一个游戏逻辑循环,除非tweenDuration完成并且我们没有进入while循环,这意味着我们已经完成了tween。我们在第 61 行设置移动部件的位置和角度,以确保变换准备好在我们的UnityAction中被引用。
tweenPiece.position = transform.position;
tweenPiece.eulerAngles = transform.eulerAngles;
OnPieceSlotted?.Invoke(this,tweenPiece.GetComponent<PhysicsPuzzlePiece>());
现在,我们进入我们的UnityAction:
OnPieceSlotted?.Invoke(this, tweenPiece.GetComponent<PhysicsPuzzlePiece>());
这看起来很有趣。为什么那里有一个问号?有一个条件运算符叫做“空条件运算符”,它在执行以下方法之前询问OnPieceSlotted是否为null。这是一种另一种语法糖。你可以通过创建一个检查OnPieceSlotted是否为null的if语句来得到相同的结果。
在UnityAction的情况下,这是说得很具体的。它询问是否有任何东西附加到这个动作上。
如果为这个UnityAction分配了方法,那么请使用以下参数调用分配的任何函数;this GameObject 和tweenPiece作为PhysicsPuzzlePiece类型。
这里发生了一些魔法般的事情。记得我们已将OnPieceSlotted分配为PhysicsPuzzleTrigger类的静态成员吗?那么,打开FinalPuzzle.cs,让我们展示静态成员的力量。
在Start方法中,我们将一个名为OnPieceSlotted的本地函数添加到从PhysicsPuzzleTrigger.OnPieceSlotted继承的静态UnityAction中。我们知道当我们的玩家将一个物体放置到正确的位置时,协程结束时需要更新它所对应的物体。是最终拼图还是入门拼图?我们通过PuzzlePieceType枚举定义了这一点:
void OnPieceSlotted(PhysicsPuzzleTrigger trigger, PhysicsPuzzlePiece piece)
{
if (piece.pieceType == PhysicsPuzzlePieceType.Intro)
{
Debug.Log("FINAL PUZZLE INTRO SOLVED. Trigger environment transition here");
tempBridge.SetActive(true);
}
else
{
numPiecesSlotted += 1;
if (numPiecesSlotted >= 3)
{
Debug.Log("FINAL PUZZLE SOLVED! Trigger portal event");
}
}
}
这个从 UnityAction 运行的本地方法为我们提供了触发器,而 piece 告诉我们是否完成了介绍性的谜题,或者是否已经处理了最终谜题。我们可以在游戏后期使用任何脚本来实现这个特定的机制,因为它静态且对我们可用。静态不仅是在地毯上穿着袜子电击你的兄弟姐妹时很有趣,在编程中也是一种魔法!
我们刚刚进行了一些中级水平的 Unity 编程。这些工具在许多情况下都可以使用,但它们并不总是作为你问题的答案的第一个选项那么容易想到。花点时间,逐节工作。用 GameObjects 制作一些协程。看看你能否在单个脚本中创建自己的 UnityAction,就像我们上面展示的那样。测试静态方法,看看它们是如何工作的,随着时间的推移,这些工具将在你开发游戏时变得自然。
摘要
多么充实的一章!我们讨论了很多内容,所以我认为我们需要在这里做一个简短的总结。物理学的概念本身就是一个很难对付的主题。我们在游戏中使用它进行小规模的模拟。我们全面介绍了 Rigidbody 组件,然后深入研究了全新的 C# 工作。对于 C#,我们讨论了:
-
执行顺序
-
静态方法
-
UnityAction(委托) -
协程
所有这些新概念都是你下一个项目中可以使用的工具。花尽可能多的时间来消化这些概念。你将在你工作的几乎每个项目中看到它们的使用。
在下一章中,我们需要添加菜单系统和用户界面,以便用户对游戏玩法有更多的上下文。
第八章:用户界面和菜单
视频游戏屏幕上排列的视觉信息和组件集合被称为用户界面(UI)。一个直观的用户界面和菜单系统为玩家提供了优质体验的机会。这种互动性和游戏可玩结果的直接影响被称为玩家代理。为这种代理设计是创建游戏世界中直观且成功的交互体验的关键。这种代理允许玩家与游戏的叙事互动,并准确地在游戏空间中参与。
您游戏中的用户界面和菜单系统也提供了玩家便利性。玩家便利性是与玩家沟通如何在游戏中使用对象,传达控制方式,并从游戏开始到结束导航游戏世界的方式。
游戏菜单系统特别赋予玩家对各种游戏模式的控制权。这些游戏模式向玩家表明何时开始,以及游戏开始前、进行中和结束后可用的选项和动作。让玩家进入游戏很重要,但在游戏过程中,您的界面可能对体验更为重要。
用户界面的四种形式是:叙事性、非叙事性、空间性和元。花一些时间来分解这些 UI 定义将更好地理解我们将在项目中如何使用它们。然后我们将查看每个的脚本编写,以提供一个正确实现它们的想法。
本章将涵盖以下主题。
-
定义 UI
-
UI 元素
-
我们项目中的 UI
-
Unity 画布系统
-
Unity UI 对象
让我们先解释一下用户界面。
用户界面
用户界面的需求是一把双刃剑。您需要设置用户界面功能以使体验继续进行,但如果不正确执行,这也可能很容易让玩家从该体验中分心。并不是总有一种机制可以用来教玩家如何与他们正在玩的世界互动。这可能会破坏沉浸感,这并不总是坏事,但需要理解如何在不破坏体验的情况下打破这种沉浸感。
我们将讨论 UI 的四种形式,这些形式被分为两个定义空间,叙事和内部。叙事适合 UI 驱动的叙事,而内部是游戏世界本身的功能性 UI。
当阅读用户界面的各种形式时,请记住,这些并不是详尽的解释,而更多的是作为一个工具来帮助设计适合您希望提供的体验的正确 UI。
当我们正在通过 叙事界面、非叙事界面、空间界面 和 元界面 形式时,我们将解释 UI 如何适应一个简单的 2x2 图表,该图表表示内部和叙事功能。下面的 2x2 网格,在 图 8.1 中,是 UI 整体视图的视觉表示,以及如何将其整合到整个游戏体验中。在接下来的段落中,UI 形式的每个部分标题也将补充一个“如果这样,那么就那样”的双答案响应。

图 8.1:2x2 用户界面设计
在上面的 2x2 网格上用 是 或 否 回答叙事和内部问题有助于我们了解需要哪种 UI 形式。随着我们详细描述这四种形式,请跟随我们的描述。
叙事 – 叙事是,内部是
将内部和外部空间融合的用户界面被称为 叙事界面。这种类型的界面承诺在提供玩家所需信息以理解内部游戏空间的同时,不会打破沉浸感。
你可能想要传达玩家需要前往的位置,但同时又想给人一种困难的感觉。在故事中,你可以给玩家一个方向并提供指南针。当你按下按钮拉起指南针时,这就是在不脱离内部空间的情况下给玩家提供信息。我们将在讨论剩余的四种类型时考虑这个指南针,看看我们能否将其转换为另一种类型。
现在我们已经解释了叙事界面形式,让我们来看一个在已发布游戏中存在的优秀例子。当解释叙事界面时,会想到一个令人毛骨悚然的电子游戏,名为 Dead Space。艺电(Electronic Arts,简称 EA)的 Visceral Games 工作室(于 2017 年 10 月 17 日解散并合并到 EA Vancouver 和 EA Montreal)创造了一款粗糙、生存宇宙恐怖视频游戏,该游戏从其他恐怖作品如 Resident Evil 4 和 Silent Hill 系列中汲取灵感。
Visceral Games 的游戏设计师需要考虑玩家如何能够将视线集中在屏幕的正中央,并尽可能多地集中注意力在那里。这样,玩家可以同时见证 Dead Space 世界的丑陋、跳跃惊吓、血腥和恐怖,并导航 Isaac Clarke 的叙事。Isaac 是 Dead Space 的主角,也是那位不幸的太空船系统工程师,在多年的时间里被卷入各种不幸的情况。
在一个角色扮演游戏中,你如何做到让玩家需要知道的大量信息得以呈现?你将重要的玩家信息放在角色本身上。这样做的话,信息就会变成屏幕上的第三人称视角,让玩家仍然能够看到屏幕及其环境。艾萨克的健康指示器位于他脊柱上发光的节点上,如图 8.2所示,而静止计则集成在他的右肩胛骨上,呈现为一个部分发光的圆形环。现在玩家不需要从主要角色上转移视线就能知道他的健康和角色统计数据。

图 8.2:死亡空间健康可视化
非叙事式 – 叙事否,内部否
看着网格,你可能想知道,如何拥有不在叙事或游戏空间中的 UI?这是一个很好的问题,而且比你想象的更常见!几乎每个菜单系统和不集成抬头显示(HUD)都是非叙事式的,如图 8.3所示。在游戏中按下播放键不是游戏叙事的一部分,但它确实是游戏的一部分,也是重要的一部分。

图 8.3:Forza 非叙事式 HUD
让我们思考一下罗盘,看看我们能否将其转换为一个非叙事式 UI 元素。它的目的是帮助玩家知道去哪里的方向。我们能否在不让游戏角色意识到它的情况下做到这一点?你可以制作一个迷你地图,显示玩家需要去的方向,并在屏幕上将其形状设计成罗盘。既然已经定义了这一点,我们就决定可以将罗盘转换成非叙事式形式。在生产的许多例子中,非叙事式 UI 元素有很多,但我们最喜欢的一个是在赛车游戏 UI 中。Forza有一个干净的 UI,显示了你的档位、速度和在世界中的位置,以帮助你沿着迷你地图上的路径前进。
空间 – 叙事否,内部是
这是一个有趣的用户界面设计案例。空间 UI 存在于游戏世界中,但游戏内的角色并没有意识到它们的存在。
再看看罗盘,也许我们希望它具有空间感。我们如何在角色没有意识到的情况下传达我们需要去的方向?
地面上可能有一个投影罗盘,显示下一个航点或目标的方向。它只有在玩家从角色上方俯视时才会出现,所以它不会总是干扰游戏的整体玩法。游戏中最好的空间 UI 元素之一在暗黑破坏神中。地面上放置的物品有一个彩色尖塔来表示特定的物品类型,而物品的名称则描述了该物品可能是什么,如图 8.4所示。

图 8.4:流放之路空间物品名称
游戏中的角色并不知道这个屏幕,但它位于游戏空间中,因为你需要将鼠标移过它才能查看。
元 – 叙事是,内部否
对于用户界面来说,元界面很有趣,因为我们不能在游戏世界中拥有界面,但角色需要了解它。如果我们看看我们的指南针示例,并尝试将其转换到元空间,我们需要更深入地思考。在角色意识到场景的同时直接与用户互动,打破第四面墙是相当独特的。让我们试试看。
屏幕的外围区域包含指南针度数,并跟随角色的旋转。角色查看他们的指南针,你可以看到由于屏幕 UI 的原因,方向是否接近正确的位置。这很麻烦,并且感觉不直观。
在第一人称射击游戏中,元界面的一个更好的例子。你有没有玩过 FPS 游戏并且被击中过?屏幕周围会出现血迹和红色的边缘。角色知道他们被击中了,通常会发出声音;元 UI 让玩家知道如果他们继续被击中,就有死亡的可能性。显示给玩家的摄像头不是玩家的视角,我们知道这一点,但我们的渲染通过带有血迹的边缘,使其变暗并增强,给人一种戏剧性的生命终结焦虑感。

图 8.5:使命召唤元界面 UI
我们刚才讨论的是设计方法,以了解您的 UI 元素向用户显示什么。如图所示,有几种方法可以分解 UI 元素。现在我们将讨论游戏 UI 开发中的一些常见术语。
UI 元素
在任何游戏中都会使用到一些常见的 UI 元素。无论是主菜单、库存系统、生命值表示,还是空间物品交互系统,它们都只有一个目的:尽可能多地给玩家提供信息,同时不会过多地影响他们的沉浸感,以免让他们从体验中脱离出来。
在接下来的几节中,我们将涵盖之前提到的主题。这些是用户界面的通用术语,不应被固定化。这些是针对游戏开发用户界面部分现有常见主题的设计思路。
在考虑未来的游戏项目时,将这些部分用作设计参考。我们将从主菜单开始;巧合的是,这是玩家首先看到的菜单。
主菜单
当游戏加载完成后第一次弹出游戏菜单时,这是开发者第一次有机会创造情感反应。你的游戏是恐怖游戏吗?字体和图像应该反映这一点。有许多设置菜单的方法。在你选择角色登录之前,你需要一个新闻屏幕弹出吗?当你按下播放时,主菜单直接进入游戏吗?游戏是否专注于菜单系统,需要多层菜单?所有这些问题都是合法的。
建立菜单系统的最简单方法就是确保它不会使游戏难以以预期的难度或连接性进行游戏,如果它是多人游戏的话。我们倾向于称之为“低门槛”。如果玩家想进来按下播放而不查看设置,他们应该能够做到。这包括查看推荐规格并构建系统以允许这样做。
玩家的体验不应该依赖于他们理解自己的系统可以处理什么。思考这个问题的好方法是从街机或游戏机中的游戏体验。PlayStation 和 Xbox 要求游戏开发者确保帧率要高,以保证体验达到良好标准。PC 和移动设备也应该如此。
背包系统
还有其他类似但不是初始玩家体验一部分的菜单系统形式。角色扮演游戏(RPG)通常使用一种背包系统,以盔甲或装备的形式显示你存储在角色上的物品。这可以用作瓶颈系统,迫使玩家回到城市出售或升级他们的装备。它也可以用来帮助定义体验,因为角色在漫游世界时不可能同时携带 30 套盔甲和 200 件武器。这是试图在打破沉浸感和保持现实感之间找到平衡。
一些有趣的背包系统形式是任务日志和成就。任务日志只是可以完成或通过完成所需任务删除的任务清单。成就则相反,通过执行某些任务来获得。
健康表示
健康可以表示为“剩余生命”,例如在《超级马里奥兄弟》中。它也可以表示为你可以被击中的次数,例如在上述《死亡空间》的参考中。它甚至可能不是通过一个特定的值来表示,而是通过屏幕上的血量,如在《使命召唤》中看到的那样。更抽象的是,不是健康,而是一个屏幕上剩余时间的计时器,显示你完成任务或关卡还有多少时间。所有这些都可以被认为是健康表示,并且可以在屏幕上使用我们之前提到的任何形式来显示。
物品交互系统
在你的游戏中,可能会有一些物品需要玩家获得一些帮助才能知道它们是可以交互的。有两种主要的方法来处理这个问题,它们都可以是空间性的。有时这将是非叙事的,我们将在下一节中讨论。
一种方法是在屏幕上创建一个仅在鼠标或十字准星覆盖物品时才可用的工具提示。这通常是在你希望某物与特定物品相关时。这可以在工具提示在屏幕空间中完成时进行——这意味着它始终是相同的大小,更像是浮动窗口。这与上面 Path of Exile 图像中展示的概念相同。你也许还会看到在物品周围或上方浮动的一个图标。这可能是为了让玩家知道他们可以与之交互。这些在本质上相似,但屏幕空间表示它不是世界的一部分,角色也不知道它的存在。这使得它是非叙事的。第二个例子是图标在物品上方浮动,它是空间性的,因为它在世界中显示出来,但角色并不知道它的存在。
我们项目中的 UI
我们的项目不是用户界面密集型的。我们故意想要尽可能保持轻量,以在环境中创造紧密的沉浸感。为了尽可能保持轻量,我们有三大部分要讨论。
-
主菜单
-
退出菜单
-
空间 UI
首先,我们将讨论主菜单以及它是如何从游戏开始时就让我们沉浸其中的。
主菜单
我们的主菜单将主要是一个非叙事菜单系统。从应用程序开始,Myvari 将在森林中看着她的书。菜单将位于左侧,提供标题、播放和退出选项供选择。当按下播放按钮时,会有相机移动和一个小型电影动画,触发游戏的开始。从播放按钮按下后, cinematic 动画结束,Myvari 开始她的空闲动画,这时我们的角色就会获得控制权。这个系统给人一种感觉,就像是在这个世界中一样,因为相机在场景转换时不会变黑,但它既不是世界的一部分,也不是叙事的一部分。Myvari 并不知道菜单系统的存在,它也没有以任何方式影响游戏世界,因此它是非叙事的。下面我们展示的 图 8.6 是一个没有所有艺术作品的草稿,以说明逻辑。这在游戏开发中是一个常见的策略。在接下来的章节中,我们将讨论实际 UI 的实现。

图 8.6:主菜单草稿
我们喜欢这种 UI 的概念,它能让玩家感觉他们正在玩的游戏立即就能沉浸其中。当你点击播放时,菜单应该消失,摄像机应该移动到一个位置,然后你就可以控制主要角色。目标是不要有加载屏幕。尽可能让玩家保持参与。
逃脱菜单
为了尽可能提供沉浸感,我们希望利用我们角色个性的核心特征之一:探索。对我们来说,这意味着我们需要让她的右腰上的书成为她游戏体验中行进的一部分。我们也知道我们需要在游戏中设置某个地方,我们也可以把设置放在书中。这是空间性的,因为它打破了游戏的沉浸感,因为设置不是叙事的一部分。当 Myvari 翻到日记本的选项部分时,这会感觉足够不连贯,但对于习惯于玩游戏的人来说却是熟悉的。这部分将是空间性的,因为它属于世界的一部分,但 Myvari 不知道这是一个关闭游戏的菜单。当她处于左侧面板时,这些都是故事驱动的元素,是世界的组成部分,Myvari 和玩家都在用它作为前进游戏的线索。在这种情况下,我们将称这个菜单部分为“存在”,因为我们将会选择艺术作品,好像来自 Myvari 种族的人制作了这本书。
我们将如何实现这一点是通过一个小型的电影动画,Myvari 拉出书并打开到日记本,这会根据你在游戏中的位置进行小更新。这本书有艺术作品看起来像她没有在其中写东西,但她的种族中的另一个人写了。这本书很旧,并引导她来到这个洞穴。将会有带有小笔记的标记来帮助玩家指引,如果需要的话。这是一个线性进程游戏,所以我们将在每个里程碑或子里程碑时更新这个内容。如果她站着不动,我们也会让她拿出书并阅读,这将使书的沉浸感与她的日记更紧密地结合,以尽可能保持体验的一致性。

图 8.7:日记本 UI 原型
日记本对我们来说是一个有趣的菜单系统。它不仅作为逃脱菜单,还向玩家提供了更多关于 Myvari 正在经历的游戏参与度的线索。图 8.7 上方展示了我们的原型,我们用它来可视化它可能的样子。这有助于我们了解如何放置摄像机,同时也帮助动画师知道如何动画她从枪套中拿出书。
空间提示
在设计玩家反馈时,有相当多的选项,正如我们在指南针问题中看到的那样。在我们的案例中,我们考虑了如何最好地展示与环境交互的能力。我们决定采用一个空间系统。这个系统将以所谓的工具提示的形式出现。这个工具提示是一个小图标,位于玩家可交互的 GameObject 之上的世界空间中。我们选择使用空间系统来保持项目在世界的空间内,以便为 UI 元素提供空间上下文;然而,我们不想让它成为叙事的一部分。这允许我们使用一点沉浸感破坏,与游戏的其他部分形成鲜明对比。当玩家看到工具提示弹出时,这将很有趣。我们可以在整个垂直切片中使用这个系统!我们正在创建一个简单的关键物品示例,它将在游戏世界中漂浮,但 Myvari 不会知道它的存在。这允许我们创建一个健壮的系统;如果我们选择为不同类型的交互使用不同的按钮,我们只需更换正确的按钮图标即可。

图 8.8:空间 UI 原型
这个非常粉红色的圆圈只是一个物品占位符,稍后将成为我们的指示器。由于它是明亮的粉红色,所以后来不会将其误认为是“完成”的物品!
我们已经讨论了用户界面的定义,并解释了我们项目对 UI 的使用。现在我们需要花些时间来回顾我们是如何使 UI 工作的。
Unity UI
在我们完全深入到我们项目中的 UI 实现之前,我们将回顾 Unity UI 系统的基本知识。这将让你了解我们在系统中使用哪些项目,以及一些我们未使用但在你以后的项目中可以使用的项目。使这个系统工作有两个主要部分:
-
Unity 画布系统
-
Unity UI 组件
在我们开始用代码实现 UI 之前,我们需要先详细了解一下 Unity 的画布系统,这样你就可以在尝试添加艺术之前对其内部工作有一个良好的基础。
Unity 画布系统
Unity 将其 UI 放置在画布系统中。这是一个具有默认多个组件的 GameObject。要创建画布,在层次结构窗口中右键单击,然后选择UI,然后画布。这可以在下面的图 8.9中看到。

图 8.9:创建画布的菜单
当这个创建完成后,你将有一个Canvas GameObject 和一个Event System GameObject。如果在该级别上已经有一个Event System,则只会创建Canvas。
Canvas有一个Rect变换,它还有一个Canvas组件、一个Canvas Scalar组件和一个Graphic Raycaster组件。我们将详细探讨每个组件,以解释它们的作用。
如果场景层次结构中已经存在另一个,则还可以创建一个Event System。这将容纳对 UI 的输入消息。
如果你正在使用新的输入系统,请确保点击此处,并将StandaloneInputModule替换为InputSystemUIInputModule。这允许事件系统知道项目中正在使用哪些输入系统。
我们为什么不逐个查看组件,从Rect变换、Canvas、Canvas Scalar开始,然后更详细地查看Graphic Raycaster呢?
Rect 变换
画布本身有一个Rect变换,但它的目的是作为其他 UI 的父级,所以它的Rect变换需要是只读的。在画布上右键点击,选择UI > Button,在画布内创建一个子按钮,这样我们就可以清楚地查看Rect变换。
在下面的图 8.10中,你可以在检查器中看到按钮的Rect变换组件,你可能期望看到常规的Transform组件。我们仍然在我们的Rect变换中有位置、旋转和缩放选项,但我们还有宽度、高度、中心点和锚点。
当与 UI 一起工作时,最好将缩放设置为1、1、1。这允许画布在需要时设置缩放。通过宽度值和高度值进行尺寸更改是最安全的。
旋转将从中心点位置旋转,这是一个小蓝色圆圈,可以通过中心点字段的值进行更改。

图 8.10:Rect 变换组件
位置字段将设置 GameObject 的本地位置。当你需要更改 UI 元素的尺寸时,最好使用Rect工具而不是缩放。在场景视图中,有一个Rect工具按钮,如图 8.11 所示,它允许你更改 UI 的尺寸,这将更新位置、宽度和高度。

图 8.11:在所选按钮上使用的 Rect 工具
UI 元素的中心点是一个x或y值,它是元素宽度和高度的归一化值。这意味着两个值都是 0.5 将中心点放置在宽度和高度的 50%,即项目的本地中心。
最后一个独特的项目是锚点。锚点设计用来允许 UI 元素保持位置不变,即使画布缩放。这可能会发生在你有多个设备或分辨率变化的情况下。有锚点选项最小值/最大值,这将设置每个锚点为其相应的归一化值,类似于中心点位置。手动这样做有时需要一点时间,所以我们有一个方便的工具来简化这个过程。如果你点击Rect 变换的左上角,它将打开一个有用的工具,允许你从常见的锚点选项中进行选择。
这看起来就像下面的图 8.12。

图 8.12:锚点常用选项
这个工具允许你选择你正在工作的 GameObject 最常用的锚点位置。主要有两种锚定类型:位置和拉伸。这个工具中间的 3x3 网格将确保相关的 UI 在屏幕分辨率不同于你构建的分辨率时不会拉伸或改变。只有在分辨率不会发生剧烈变化的情况下,这是一个好选项。第二种类型是拉伸,位于右侧和底部边缘。如果你的游戏是以 1920x1080 分辨率构建的,并且玩家选择在超宽显示器上玩游戏,你可能希望允许某些 UI 元素进行一些缩放。如果是一个 16:9 宽高比的 4k 显示器,那么你可能需要考虑拉伸所有元素;否则,UI 将显得非常小。
锚定是一种艺术形式。上面概述的技巧会对你大有裨益。正确锚定的最佳方法是使用编辑器玩游戏并调整大小。它可能不会给你每一个场景,但它会给你一个很好的视角,了解 UI 元素如何对分辨率变化做出反应。
画布组件
画布组件只包含一些选项,但它们至关重要!在下面的图 8.13中,你可以看到我们将要讨论的部分。

图 8.13:画布组件
我们有渲染模式,下面有一些选项:像素完美、排序顺序和目标显示。之后,我们有附加着色器通道。让我们逐一查看这些选项。
渲染模式
可以选择三种渲染模式:屏幕空间 - 覆盖、屏幕空间 - 摄像头和世界空间。它们各自有特定的用途,游戏可以在其世界中拥有多个画布,以满足其需求。随着我们逐一介绍它们,考虑一下我们如何在我们的当前项目中使用它们。在描述完所有 Unity UI 的功能后,我们将进入实现阶段。
屏幕空间 - 覆盖
这是一个常见的画布渲染模式。这个模式的好处是它可以在自己的场景中使用,并在运行时以增量方式加载到你的游戏中。这使得你可以轻松地创建与 PC 监视器菜单系统分开的移动菜单。这非常有效;然而,它应该只用于简单的 UI。如果你打算拖动 UI 元素或从鼠标上下文(如悬停)动画它们,那么最好使用摄像头选项。
这种类型画布的一个好例子是主菜单或 HUD,它不太具有交互性。
屏幕空间 - 摄像头
与叠加选项一样,如果你将要制作利用EventTrigger类的函数,这是一个非常好的模式。你也不能像叠加模式那样实例化它。它必须已经在场景中,并且有一个它将引用边界范围的相机。它将附着到相机上,所以如果你做出这个更改而它从你那里消失,双击你的相机,它就会出现在那里!
这种模式的绝佳例子类似于 ARPG,你需要拖放装备来装备物品。
世界空间
当你需要一个位于世界空间的菜单时,这种画布渲染模式会被使用。最好的解释方式是通过最佳用例。当你想在空间中角色的头上显示聊天气泡时,你会使用它。你可能希望在 UI 中可选择的标志,这可能会使用一个世界空间画布。如果这个标志有文本或其他形式的 UI 附加到它上,那就更好了。
渲染模式选项
在渲染模式下方有三个选项:
-
像素完美 – 仅在你在 2D 空间内工作,且 UI 需要精确到每个像素时使用。它有助于将 UI 开发到创建中的像素限制。
-
排序顺序 – 默认排序顺序设置为在画布下的层次结构中工作。一个项目在层次结构中的位置越高,它将被渲染得越早。你可以通过输入一个值来覆盖它。较低的值将首先渲染。较高的值将被发送到列表的较低位置。如果你想让一个单独的项目始终在后面,只需将值设置为
999,它将始终在其他人之后渲染,无论层次结构顺序如何。 -
目标显示 – 如果你需要第二个显示器的另一个 UI,则应使用此选项。你可以将其设置为仅在第二个显示器上显示。这可以用于多达八个显示器。这种用例可能是类似于赛车游戏的游戏,这些游戏通常使用三个弯曲的显示器。
额外的着色器通道
当处于叠加模式时,UI 通常不会包括法线、切线等。使用图 8.14中显示的下拉菜单来选择它们。

图 8.14:额外的着色器通道选项
如果你需要在 UI 元素中特别需要它们,则需要选择这些。否则,请将其保留为无。
画布缩放器
此组件负责确保具有此组件附加的 GameObject 下的所有子 UI 对象的比例正确。它不仅负责缩放 UI 本身,还负责字体大小和任何附加到图像上的图像边框。
Canvas Scaler 组件有几个独特的参数。它们根据选择的 UI 缩放模式放置在窗口中。有三种 UI 缩放模式。
常数像素大小
当你需要保持像素大小不变,而屏幕发生变化时,会用到这个选项。这个用例是如果你知道你将使用单一分辨率来玩游戏。如果你的游戏可以缩放,那么你必须通过动态设置缩放因子,并确保每单位像素相同来工作。这些参数在下面的图 8.15中可以看到。

图 8.15:画布缩放组件常量像素大小 UI 缩放模式
如果你认为你的游戏在任何时候都会进行调整,那么请考虑使用根据屏幕大小缩放选项。
根据屏幕大小缩放
当你选择根据屏幕大小缩放时,与常量像素大小选项相比,会出现不同的参数。如下面的图 8.16所示,我们有参考分辨率、屏幕匹配模式、匹配滑块和每单位参考像素。

图 8.16:画布缩放组件根据屏幕大小缩放模式
-
参考分辨率 – 你期望最常用的屏幕分辨率。从那里,它将根据玩家可能使用的不同分辨率进行缩放或放大。
-
屏幕匹配模式 – 包含三个选项:
-
匹配宽度和高度 – 当宽度和高度发生变化时,这将允许应用程序匹配宽度和高度的混合比例。整体来说,这个选项效果不错,直到你遇到超宽显示器。这也是唯一一个匹配滑块可用的选项。在接下来的两个选项中,这个滑块将不可见。
-
扩展 – 这意味着画布会放大,但不会小于参考分辨率。这对于扩展宽度和高度的需求来说非常出色。这是我最喜欢的选项之一。
-
缩小 – 这个选项与扩展选项类似,但它会缩小而不是超过参考分辨率。这效果很好,但你必须从高分辨率开始工作。
-
-
每单位参考像素 – 这个选项指的是每厘米有多少像素(这是一个 Unity 单位)。当你使用精灵选项制作 2D 游戏时,这一点非常重要。如果你的精灵设置为每单位 100 像素,而此每单位参考像素设置为 50,则你的精灵将比预期大两倍。
常量物理大小
这与常量像素大小模式类似;然而,它使用物理单位,如下面的图 8.17所示。你可能更习惯于使用这些单位而不是像素来设置大小。

图 8.17:画布缩放组件常量物理大小
如果这些单位更适合您使用,请确保您更改所有字体的缩放比例到这些刻度。物理选项的列表如下所示,在图 8.18中。

图 8.18:物理单位选项
使用这些选项中的任何一个都会迫使您更改所有 UI 项目,以适应相同类型的刻度单位。例如,以像素为单位的大小通常是 300 宽,而 300 厘米则非常巨大!刻度可能应该是 0.1。因此,我们建议您在您的系统中进行工作,并了解如果您想从一开始就使用这种缩放模式,您将使用哪种刻度。
最后一个组件是Graphic Raycaster。这是与画布一起提供的倒数第二个默认项目。让我们解释一下Graphic Raycaster是如何与画布一起工作的。
Graphic Raycaster 组件
此组件是在画布上创建的。这样做的原因是,它是您鼠标点击的功能。以下是在图 8.19中Graphic Raycaster的可用参数:

图 8.19:GraphicRaycaster 组件
这里有三项参数需要快速浏览。
-
忽略反转图形 – 此参数确保您不能点击被反转的对象。请记住,相机中裁剪了背面。您可以通过翻转它们来关闭 UI 的元素,但即使这样检查,它们仍然可以点击。
-
阻挡对象 – 这允许在 UI 前面的 2D 或 3D 项目阻挡点击 UI。默认为
none。 -
阻挡遮罩 – 此参数允许您放置图层以阻挡 UI。由于 UI 是精灵,它们通常是矩形,并且可以相当容易地重叠。为了解决这个问题,您可以创建一个 UI 阻挡层,这将允许您放置对象在前面以阻挡点击,即使它是不可见的,alpha 值为 0。
我们花时间介绍了这些默认项目,因为它们是您开始使用 Unity 的 UI 时将看到的初级项目。随着您创建更多的 UI,您将有机会学习更多选项,但这个基础将帮助您开始。接下来,我们将探讨一些要添加到画布中的 UI 对象。
Unity UI 对象
我们现在有一个画布了!这很好,因为我们已经了解了它是如何与动态分辨率一起工作的,以及如何为您的游戏需求设置它。现在我们需要添加一些对象,使其变得有用。Unity UI 对象分为两种类型:视觉和交互。
视觉元素正是您所期望的。它们是仅作为视觉元素的项目,但它们可以附加到交互元素上。以下是一些这些对象的示例,包括描述和视觉示例:
- 图像 – 有两种图像类型:原始图像和图像。原始图像仅用于您不需要边框的地方;然而,通常最好只使用图像对象。图像可以接受精灵,并且可以为其添加边框。您还可以在检查器中的图像组件内着色精灵。还有一个名为面板的另一个 UI 选项。这是一个带有图像组件的 UI 对象,设计为 UI 面板。图像和面板之间的唯一区别是面板默认将设置为拉伸并填充整个画布。

图 8.20:默认图像和图像 UI 组件
- 遮罩 – 遮罩组件将剪除其下方的 GameObject。这对于遮罩掉可能不想看到的下方额外项目非常有用。下面,我们在图像上添加了一个遮罩,并在其下方添加了另一个图像。轮廓是遮罩;应该为正方形的图像由于遮罩而顶部和底部被裁剪。

图 8.21:来自图 8.20 的遮罩默认图像
- 文本 – 这就是文本!有时这也被称为标签。如果您需要,您可以为您的 UI 添加特定的字体。当您创建它时,您将在文本选项后看到TextMeshPro。这是由于TextMeshPro(TMP)非常受欢迎,以至于它已被集成到 Unity 的核心功能中。

图 8.22:TextMeshPro UI 组件
交互式项目可以包含视觉元素,但它们带有交互UnityEvents。以下是一些示例,包括描述和视觉示例:
- 按钮 – 这个交互对象在其层次结构中默认带有标签。它还带有点击时的
UnityEvent。如果它被高亮、按下或禁用,它具有着色能力。这是 UI 交互的主要功能。

图 8.23:按钮 UI 组件
- 下拉列表 – 下拉列表是一个用户可以选择的预定义选项组字段。当用户更改此值时,它将触发
OnValueChangedUnityEvent。

图 8.24:下拉列表 UI 组件
-
输入字段 – 这是一个标准的输入字段,用户点击进入或“聚焦”到它。有一个我们想提到的有趣属性,称为内容类型。这允许开发者进行错误检查,而无需编写代码。例如,将其设置为整数数字将只允许用户输入数字。这个交互式对象有两个
UnityEvents:-
OnValueChanged– 每次发生更改时,这将返回当前输入值中的字符串 -
EndEdit– 当用户点击其他位置或失去对该输入字段的关注时,这将返回字符串![图形用户界面,文本,应用程序描述自动生成]()
图 8.25:输入字段 UI 组件
-
-
滚动条 – 滚动条通常与滚动
矩形结合使用。其目的是在需要可能很大的字段时作为滚动条。无论滚动条有多大,值都是从 0 到 1。它可以垂直或水平。它还有一个UnityEvent,可以用来知道OnValueChanged,这样你就可以在移动滚动条时返回值。

图 8.26:滚动条 UI 组件
- 滚动 矩形 – 这也可以称为滚动视图。如果需要,可以与两个滚动条结合使用,以设置垂直和水平滚动。这还通过一个遮罩来隐藏遮罩本身之外的信息。它还在滚动
矩形的滚动上有一个OnValueChanged UnityEvent。

图 8.27:滚动矩形 UI 组件
- 滑块 – 这是一个带有可拖动对象的滑块,它将滑块的值从你设置的最低值和最高值设置。它还有一个
UnityEvent,从那个最小值和最大值返回OnValueChanged。

图 8.28:滑块 UI 组件
- 切换 – 这是一个带有标签的复选框。点击时,可以使用
OnValueChanged UnityEvent来评估它是开启还是关闭。

图 8.29:切换 UI 组件
- 切换组 – 如果你将切换添加到一组中,你可以设置该组只允许组内一个切换可被选择。如果你在分配的分组中选择了另一个切换,它将关闭之前开启的切换并开启所选的切换。有一个允许关闭选项,它允许你选择当前选中的切换以取消选择所有组。与该组本身连接的
UnityEvent没有唯一性;然而,每个切换仍然有自己的OnValueChanged事件,这将触发。一个小提示,如果你要创建一个切换组,请确保每个切换在其切换组件中分配了该组。

图 8.30:切换组 UI 组件
这些都是 Unity UI 中可用的 UI 项的好例子。从这里,我们需要通过 Unity UI 的实现来适应我们的游戏。我们之前讨论了设计;现在我们需要查看代码以了解当玩家需要与之交互时它是如何工作的。
实现
我们现在需要查看我们的实现。了解所有 UI 对象的外观和它们的目的很有帮助,但我们现在需要看到它们在实际中的样子。我们将从游戏开始前玩家的主菜单开始。之后,我们将进入日志或逃生菜单。然后我们将完成与游戏机制交互的空间 UI。
在阅读这部分内容时,请记住,我们不会覆盖脚本的所有行,因为此时我们假设你已经习惯了查看我们在 GitHub 上的代码。
如果在任何时候你对本书如何布局来解释代码感到困惑,确保你调出被引用的脚本并重新调整你的方向。我们以这种方式解释代码的主要目标是尽可能简洁地说明我们在做什么以及为什么这样做。查看每一行代码并不能帮助达到这个目的!
话虽如此,让我们进入主菜单的实现。
主菜单实现
由于我们希望这个菜单是非叙事性的,但位于世界空间中以产生空间错觉,我们选择使用世界空间画布。在下面的图 8.31中是层次结构和带有未更改的折叠组件的检查器。

图 8.31:左,MainMenuCanvas 的层次结构;右,Canvas 的检查器
MainMenuUIControl.cs脚本是我们控制主菜单的方式。在处理 UI 时,你需要确保你导入了 UI 库:
using UnityEngine.UI;
当你使用 UI 库时,你将能够访问所有 UI 对象及其方法。尽管下一行我想要放置在这里的不是 UI 的特定部分,但我想要向你展示我们还没有讨论过的东西。这个方法被称为FindObjectOfType。我们知道场景中永远只有一个MyvariThirdPersonMovement类,所以我们使用这个方法来获取它,然后请求它的父级,这样我们就知道玩家根。
playerRoot = FindObjectOfType<MyvariThirdPersonMovement>().transform.parent;
我们还需要禁用角色并为事件系统设置监听器,这样它就知道当我们点击画布中的按钮时要做什么。
要禁用角色,我们有一个我们称之为on awake的方法来关闭我们需要关闭的东西。当使用 Cinemachine 时,你想要禁用所有可用的相机,否则 Cinemachine 会转到其中一个相机。然后我们只禁用玩家的控制脚本。这允许角色的动画在原地继续播放,但我们无法控制她。
在awake时:
SetPlayerEnabled(false);
在第 50 行有一个独立的私有实现:
void SetPlayerEnabled(bool enable)
{
CinemachineVirtualCamera[] cams = playerRoot.GetComponentsInChildren<CinemachineVirtualCamera>(true);
foreach (CinemachineVirtualCamera cam in cams)
{
cam.gameObject.SetActive(enable);
}
playerRoot.GetComponentInChildren<MyvariThirdPersonMovement>().enabled = enable;
}
我们之前已经设置了几次监听器,但让我们也看看它们:
startGameButton.onClick.AddListener(OnStartGameButtonPressed);
quitButton.onClick.AddListener(OnQuitButtonPressed);
这里发生的事情是,对于startGameButton和quitButton中放置的相应按钮,当它们被点击时,将激活它们监听器中的方法。
OnStartGameButtonPressed方法看起来是这样的:
void OnStartGameButtonPressed()
{
SetPlayerEnabled(true);
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
this.gameObject.SetActive(false);
}
当它被按下时,将角色设置为启用,这样我们就可以使用输入来移动她,锁定并隐藏鼠标光标,并禁用主菜单,这样你就再也看不到它了。如果你点击退出按钮,你将关闭应用程序。Unity 有一个简单的方法来退出应用程序:
Application.Quit();
这就是整个主菜单!我们需要的最难的部分是锁定玩家。否则,当主菜单打开时,他们将是可移动的,而这在我们这个情况下不是我们想要的。接下来,我们需要处理日志。
日志实现
在大多数游戏中,有一个常见的概念,即逃生菜单。也就是说,当你按下Escape键时,你会遇到一个通常暂停游戏玩法的菜单。在我们的情况下,我们希望当你按下Escape时,我们的角色会打开她的书并查看。这将很好,因为它允许游戏暂停一下,当相机移动到书本附近时,我们可以放置正常的逃生菜单选项,如继续和退出游戏。在这里,主菜单中的一些类似概念也会出现,比如锁定和解锁光标。还有一个玩家启用方法,它与主菜单中的方法相同。
下面,在图 8.32中,是日志 UI 检查器中层次结构和脚本的另一种表示。在公共字段中,我们使用输入系统而不是仅仅依赖鼠标输入,这是其中独特的一项。
为了加载日志,我们可以按字母B或Escape键。

图 8.32:左,日志层次结构;右,书籍检查器面板
这本书对我们来说是一个有趣的转折点。与这个脚本相关的所有编码工作都已完成。我建议打开脚本并查看它,以帮助记住之前的编码课程。
最后一个 UI 元素是空间 UI,它帮助玩家知道他们正在查看的项目是可交互的。让我们分解这个实现。
交互 UI 实现
这项设置是独特的,因为没有画布用于此项目。我们将有一个单独的 GameObject,它位于场景中,我们将将其移动到所需的位置,并根据我们正在查看的内容打开或关闭它,如果它是不可交互的。我们有一个简单的 GameObject,它是一个没有材质的球体,因此它是明亮的粉色。
在InteractiveHighlight.cs脚本中,on awake我们找到这个 GameObject 并获取其渲染器。如果没有找到,则我们有一个错误,告诉我们我们找不到它。
我们获取网格渲染器,这样我们就可以在我们不需要看到它时禁用它。
void Awake()
{
highlightIndicator = GameObject.FindGameObjectWithTag("InteractionHighlight");
if (highlightIndicator == null)
{
Debug.LogError("Highlight indicator not found in scene");
}
highlightIndicatorRenderer = highlightIndicator.GetComponent<MeshRenderer>();
}
现在我们有了高亮指示器,我们应该执行指示器的隐藏和移动。我们使用射线投射来知道我们是否击中了可交互的项目或游戏拼图的一部分。这是一个物理方法,所以我们将它放在固定更新中。这将确保代码按照我们在第七章“刚体和物理交互”中讨论的物理更新时间运行。
void FixedUpdate()
{
Ray ray = new Ray(transform.position, transform.forward);
if (Physics.Raycast(ray, out RaycastHit hit, maxDistance, rayMask, QueryTriggerInteraction.Collide))
{
highlightIndicator.transform.position = hit.transform.position + new Vector3(0f, height, 0f);
if (!highlightIndicatorRenderer.enable
d)
{
highlightIndicatorRenderer.enabled = true;
}
}
else
{
if (highlightIndicatorRenderer.enabled)
{
highlightIndicatorRenderer.enabled = false;
}
}
}
如前所述,这里的固定更新在物理时间上运行,并检查从屏幕中心射出的光线是否击中了两个遮罩上的项目。如果是击中并且距离在最大距离内,则移动高亮的项目并打开其渲染器。如果不是,则关闭它!
摘要
本章内容丰富,信息量大。尽管我们只讨论了三个 UI 元素,但我们不得不将其分解成所有这些部分,以帮助您在未来的项目中。您现在也将对其他游戏开发者如何为他们的游戏设计 UI 有一个强烈的认识。在第十二章“最终润色”中,我们将讨论它的清理以及如何通过润色 UI 来改善玩家的体验。在下一章中,我们将讨论视觉效果和一些粒子系统。
第九章:视觉效果
目前游戏还没有全部的功能。我们已经从概念到草图,然后将游戏开发到可玩的状态。但这并不意味着我们马上就完成了!我们需要考虑如何让玩家有更多的情感沉浸感。幸运的是,Unity 为我们提供了出色的资源和工具,让我们能够将游戏当前的状态提升到一个新的视觉层次。这是通过各种方式实现的,例如着色器、粒子和其他将在 第十二章,最终润色 中介绍的抛光工具。
这些主题非常复杂。目前,我们将通过查看着色器和粒子来概述视觉效果的主要焦点。然后,我们将进一步概述它们的更高级形式。因为我们在这个项目中使用的是 通用渲染管线(URP),我们将介绍一些重要工具,如着色器图形、VFX 图形和 Shuriken。着色器图形以视觉方式显示着色器的详细信息和编码。VFX 图形是为了帮助你理解 GPU 粒子的各种属性而创建的。Shuriken 是一个以 CPU 为重点的粒子创作工具,在所有渲染管线中都有提供。我们也会介绍这一点。
在本章中,我们将涵盖以下主题:
-
视觉效果概述
-
着色器图形
-
粒子系统:
-
Shuriken
-
VFX 图形
-
视觉效果概述
开始使用视觉效果可能会感到有些令人畏惧。我们目前有一个简单的场景。如果不花时间有意识地回答需要解决的问题,世界就不会感觉生动和沉浸。
在这本书中,你已经处理了许多通用的游戏设计问题。你能够自己回答这些问题,并且可以使用它们来完成任何你想要工作的项目。然后,你得到了我们在创建这个项目时自己发现的答案以及它将如何发展。我们对玩家的感受有了一个相当好的想法:用最简单的话说,就是幻想探险。我们现在需要能够遍历我们的场景,找到需要更多幻想的地方。探险是通过机制和叙事设计来完成的。
幻想是一个广泛的概念。我们实际上可以选择任何主题。我们决定克服这个模糊的起点,并找到了一个以古老种族为中心的轻科幻主题。这些生物紧握着他们周围自然空间中天体的力量。与自然合作,他们在某个时刻建造了一个玩家将探索的洞穴。我们需要想出一种方法来通过视觉游戏来体现这种叙事,而玩家接受自己作为这个世界的主角正是我们的目标。
为了执行这种视觉叙事,我们需要整合 Unity 中可用的多种视觉效果工具。Shader Graph 允许我们构建具有有趣属性的着色器,这些属性可以以各种方式与光线和闪烁效果互动。Shuriken 为我们提供了粒子系统,可以添加环境灰尘、围绕生物发光植物的发光粒子,以及其他幻想元素的简单细节。VFX Graph 允许我们将简单的粒子推向极限,并使 GPU 系统发挥作用。通过利用 GPU 系统,VFX Graph 将赋予我们使用许多粒子的能力。尽管这并不实用,但你可能可以生成数千万个粒子。最后,我们将使用 Unity 中的照明为玩家提供查看提示,并设定当前动作、系统或地点的氛围和基调。
为了开始这一章,我们将奠定术语的基础,并详细说明单个视觉效果工具。在这些解释之后,我们可以进一步探讨如何将 Unity 提供的工具整合到我们的工作空间中,以创建一个视觉沉浸式环境。向前推进,这一部分可能成为一个有用的参考点,以便回来重新熟悉工具及其用途。
Shader Graph
Shader Graph 是一个视觉脚本工具,旨在允许艺术家驱动的着色器创建。着色器是脚本,它告诉图形管线如何渲染材质。材质是着色器的一个实例,它为 GameObject 中的某个网格设置了参数。一个简单的例子是皮革。如果你想到皮革,你可能会想到许多不同类型的皮革。它是哪种颜色?它是闪亮的吗?它有独特的纹理吗?所有这些选项都是着色器中的参数,可以在材质中设置以正确渲染对象。
在 Unity 中,着色器是用高级着色器语言(HLSL)编写的。正确编写此代码需要详细了解你计算机中的图形管线,这可能有点令人望而却步。图形管线是一个复杂的概念模型。简化来说,计算机通过多个层次和阶段来理解场景中的 3D 视觉图形,然后根据这些信息将那些视觉效果渲染到 2D 屏幕上。
实际上,仅仅阅读上面的段落可能显得有些困惑。这是自然的。这里有很多动态部分,我们将在本章的这一部分中对其进行分解。为了不深入 HLSL,我们将专注于 Shader Graph。如果你在 Shader Graph 中花费了一些时间后想要进入更技术性的职位,我们建议学习 HLSL 并手动编写着色器。一旦你学会了 HLSL 并手动编写着色器,你将拥有一个坚实的通用着色器创建基础。
让我们先通过 Shader Graph 的设置,然后介绍如何创建一个着色器。然后我们应该花一些时间讨论创建着色器时使用的常用基本节点。节点是代码块或代码片段的视觉表示。这些节点可以相互连接,以创建更大功能的视觉分层效果。
设置
我们一直在谈论使用URP项目,该项目应该已经自动安装了 Shader Graph。如果没有安装,你可以很容易地通过前往包管理器并从 Unity 注册表包中安装它来安装它。
图 9.1 以下展示了正确安装 Shader Graph 所需的步骤。
如果你像下面图中那样有一个绿色的勾选标记,那么它已经安装好了!

图 9.1:检查 Shader Graph 是否已安装
现在我们已经安装了 Shader Graph 或者验证了它已经安装,接下来让我们继续创建你的第一个着色器。
创建
在项目窗口的空白区域右键点击会给你该空间的标记菜单。我们想要创建一个新的着色器,所以我们将鼠标悬停在创建 > 着色器 > 通用渲染管线上,然后得到四个选项。这四个选项是 URP 的基本选项,会自动为你设置着色器设置。参考图 9.2 了解创建着色器的路径。

图 9.2:在 Unity 编辑器中创建着色器的路径
你可能想知道,“我应该选择哪一个?”如果是这样,那是一个很好的问题。让我们逐一介绍这四种类型,以防你在我们做出选择后想要尝试其他类型。从上到下,我们有Lit、Sprite Lit、Sprite Unlit和Unlit着色器图来使用。让我们按这个顺序逐一介绍这些类型。
Lit Shader Graph
Lit Shader Graph 允许你使用现实世界的光照信息渲染 3D 对象。使用此着色器的着色器将使用基于物理的渲染(PBR)光照模型。PBR 模型允许 3D 表面在各种光照条件下具有像石头、木头、玻璃、塑料和金属等材料的照片级真实感。借助 Lit 着色器的帮助,这些对象的光照和反射可以准确地遵循动态变化,例如从明亮的光线到黑暗的洞穴环境。
Sprite Lit Shader Graph
URP 自带一个 2D 渲染和光照系统。这个系统将用于这种图类型,因为着色器将要渲染的对象将是精灵。精灵是一个二维位图(一个表示每个像素颜色的二进制数据数组),它被集成到一个更大的场景中。这将允许精灵接收所需的光照信息。
Sprite Unlit Shader Graph
这与上面的着色器光照着色器图类似,但与 Sprite 无光照着色器图的不同之处在于,它被认为是始终完全光照的,并且不会接收任何光照信息。此图也仅使用 URP 中的 2D 渲染和光照系统。
无光照着色器图
URP 的无光照类型图使用与光照着色器图类型相同的 3D PBR 光照模型。主要区别是它不会接收光照信息。这是 URP 中最高效的着色器。
着色器图界面
为我们的着色器图选择光照着色器图类型。当你右击项目窗口时,将创建一个新的着色器文件。双击此文件将打开着色器图窗口。
有一些内容我们需要讲解,以便你能够理解以下小节中涵盖的内容。我们需要讲解主堆栈、黑板、图检查器、主预览以及节点。在下面的图 9.3中,显示了这些部分中的四个。我们将在本章中详细讲解它们。

图 9.3:着色器图窗口分解
主堆栈
图 9.3中的绿色轮廓项是主堆栈。主堆栈分为两部分,顶点和片段。主堆栈的顶点块包含对具有此分配着色器的 3D 对象的实际顶点的指令。在此块中,你可以影响顶点的位置、法线或切线属性。这三个属性在二维和三维环境中无处不在。位置表示顶点在对象空间中的位置。法线用于计算光线从表面反射或从表面发出的方向。切线改变你表面上顶点的外观,以定义对象的水平(U)纹理方向。
在我们的案例中,我们不需要更改任何顶点属性,所以我们将继续到片段着色器部分,并保持对象空间不变。
片段指令可以被视为屏幕上可能的像素。我们可以根据对堆栈输入所做的更改来影响像素。片段着色器中列出的属性取决于我们在创建着色器时选择的着色器类型。
片段堆栈内部的块称为片段节点。如果你不需要特定的片段节点,可以通过右击片段节点单独选择删除来移除它。你也可以通过右击片段框架的底部并选择添加节点来向堆栈添加其他片段节点。
在图 9.4中,你可以看到所有要添加到堆栈中的节点选择。如果你的着色器没有设置为接受那些新的片段节点,它们将呈灰色且不可用。现在,让我们来看看光照着色器片段选项。

图 9.4:片段节点选项
片段堆栈中的片段节点代表将在裁剪空间或屏幕上潜在显示的像素。3D 对象也有其面的二维表示形式,即 UV。UV 是 3D 对象的二维纹理表示。UV 具有一个平坦的二维轴,从 0(U)到 1(V)的图表。这个特定的 UV 图表是表示在这个 UV 平面上拉伸到 3D 对象上的每个顶点的表示。UV 图表也称为 UV 纹理空间。
查看下面的图 9.5,您可以看到几何体已经被展开以使其扁平。这就像纸艺或折纸。了解这一点为着色器可以如何操纵网格的顶点以及面的颜色奠定了基础。

图 9.5:基本网格,UV 布局,基础颜色上的渐变
我们想向您展示我们如何在 Shader Graph 中实现图 9.5 中的渐变。虽然这并不是我们可以从最简单的图表开始的地方,但它很好地分解了一些关键概念。
如果您查看下面的图 9.6,您将看到我们构建渐变的图表,我们将其放置在基础颜色中。然后,将渐变应用于我们用此着色器分配的材料的 3D 对象的 0-1 空间。它不是一个复杂的着色器,因为它有从 UV 节点硬编码的渐变。
我们将在下一节中增加我们在黑板中设置的参数的复杂性。

图 9.6:测试渐变着色器
我们将在本章下一部分分解常用节点。现在,快速解释我们所做的工作将有助于分解。
基础颜色
我们正在使用两个渐变,x和y,在红色和绿色通道中代表 UV 的 0-1 空间。红色和绿色通道是颜色空间的一部分;有红色(R)、绿色(G)、蓝色(B)和Alpha(A)通道。RGB 代表颜色值。Alpha 通道表示每个像素的不透明度,从 0(完全透明)到 1(不透明)。
我们已经看到,在 Unity 中,0-1 空间从左下角开始,线性地延伸到右上角。这意味着绿色通道将是从下到上的线性渐变。分离该通道允许我们在 Lerp 节点中操作 0-1,用红色代替黑色,用青色代替白色。在接下来的几节中有很多事情要做,但请坚持下去!当我们分解节点时,将更容易逐个节点进行跟踪。
法线
法线告诉每个片段它们应该如何对击中面的光线做出反应。
这对于在不改变轮廓的情况下增加面部细节非常有用,减少了用于更高细节所需的多边形数量。查看下面的图 9.7,你可以看到似乎有凸起从立方体中拉出。这不是颜色变化;这是光线对表面反应的结果。如果你仔细看,边缘上没有凸起。这是因为立方体的形状没有改变。这只是一个立方体,由于法线贴图的作用而表现出这样的效果。

图 9.7:立方体上的法线
在图 9.7的左侧,左边的立方体由于法线贴图使用其从切线空间的 RGB 通道中的颜色方案而呈现蓝色。这意味着一个平坦的法线贴图将代表0,0,1。红色和绿色通道用于表示光线在x切线或y切线上的作用方式的变化。当我们处理第十二章中的材质时,最终润色,我们将更详细地介绍法线图的功能。
金属
金属材质听起来就是这样的。这就是这种材料的金属感!但这并不是一个很好的定义,所以让我们试着稍微分解一下。
金属字段是一个从 0 到 1 的标量浮点数,0 表示非金属,1 表示裸金属。金属材质会吸收其周围环境的颜色。在图 9.8中,我们有四个球体,它们的材质属性有四种不同的设置。我们只使用 URP/Lit 着色器来测试这些设置。对于这一部分,我们只查看左侧的两个球体。最左侧的球体是白色,金属设置为 0。这种材质没有吸收任何环境的颜色。它只吸收光照信息及其基础颜色白色。
第二个球体,尽管它仍然以白色为基础颜色,但金属设置为 1。平滑度设置为 0.5 以保持中性,你很快就会了解更多关于这一点。如果你仔细看,第二个球体有 Unity 默认的天空盒的颜色。现在我们需要在这个材质中加入平滑度。

图 9.8:从左到右:无金属,全金属,全金属不光滑,全金属全光滑
平滑度
继续查看图 9.8,我们将转向右侧的两个球体。从左数第三个球体非常有趣。这个球体的基础颜色是白色,金属设置为 1,平滑度设置为 0。这意味着整个球体是完全漫反射的!在这个例子中,漫反射意味着环境中的所有颜色在整个球体上混合,导致几乎完美的真正中性灰色。第四个球体也是一样的,但平滑度设置为 1。这意味着整个球体正在反射直接的环境。
发光
为了使一个物体发出或辐射光线,你将目光投向一个发射贴图。发射贴图的作用是能够将颜色推到比 1 更亮的值。超过 1 的值允许物体的这部分发光。这对于熔岩、科幻灯光或任何你想发出亮度的东西很有用。否则,片段节点默认为黑色,不会创建发射。

图 9.9:左无发射,右有发射,强度为 2.4,无辉光
这看起来不像一个发光的蘑菇!这是因为发射需要后处理体积。为此,创建一个空的 GameObject 并命名为_PostProcess。我们这样命名是为了给它一个独特的名字。使用下划线作为前缀,我们让我们的开发者知道这个对象只包含逻辑。在游戏中没有用于使用的 GameObject。然后添加一个体积组件,如下面的图 9.10所示。

图 9.10:添加到后处理 GameObject 的体积
我们还没有完成!我们需要添加一个配置文件和一个覆盖来设置我们的辉光。按下体积组件右侧的新按钮将创建一个配置文件以添加你的设置。这允许你存储这些设置以供其他场景使用。当你添加一个配置文件时,你将能够添加一个覆盖。
我们想要点击添加覆盖,然后后处理,最后辉光。然后选择强度布尔复选框以允许更改强度。将其更改为1。设置如下在图 9.11所示。

图 9.11:后处理体积的辉光覆盖
现在,我们看到它在屏幕上的蘑菇周围发出光线。这不是在场景中添加光线;它只是在网格外部添加一个亮度值到屏幕上的渲染中。

图 9.12:左无发射,右有发射和辉光设置
我们有一个闪亮的发光蘑菇!去添加发射吧!我们现在将探讨环境光遮蔽。
环境光遮蔽
环境光遮蔽(AO)的作用是在某些部分添加暗点以显示皱褶。即使没有特定的光源产生阴影,这也能添加一个干净、美观的阴影效果。AO 是为所有角度的光设计的。这个属性期望的值在 0 到 1 之间。如果你没有为你的模型创建 AO 贴图,最好将其设置为 1。在第十二章,收尾工作中,我们将使用 Myvari 的材料,它将包含一个 AO 贴图来覆盖。
这就是总结中的主堆栈。堆栈上的每个属性都可以用来提供独特的着色器。有助于进一步定制的还有着色器图中的黑板部分。
黑板
黑板允许用户创建可以在着色器中使用的属性和关键词,这些属性和关键词可以以各种方式动态更改。属性可以在着色器内部使用,或者通过暴露复选框将其暴露给检视器中的材质。
您可以在着色器图窗口的右上角找到黑板按钮。一旦点击此按钮,黑板将打开其自己的独立 UI。

图 9.13:黑板中可用的变量类型
有 16 种可创建的数据类型。这些数据类型可以在运行时通过脚本进行更改。关键词设计为在运行时按材质实例进行更改。由于编译器需要考虑所有变体,因此此选项只有少数几个。这对于像移动规格这样的东西很有用。您可以为不同的系统创建枚举(用户定义的约束集)或列表,以更改着色器的保真度(感知质量),以适应平台限制。
图检视器
图检视器提供了对着色器类型的选项。我们选择从 URP/Lit 着色器作为基础开始,这会在图检视器中为我们设置特定的设置。下方的图 9.14显示了图检视器。这些是 URP/Lit 默认设置的设置。

图 9.14:图检视器
我们构建的着色器中可用的每个属性在特定情况下都有很大的用途。当我们讨论在完成细节中使用它们时,我们将解释为什么我们要更改图设置。现在,了解我们选择的材质是Lit选项,它默认为金属不透明的工作流程。这意味着您看不到其后面的物品的颜色,因为它不透明。
主预览
主预览是对着色器在游戏中外观的整体查看。默认是一个球体。
您可以在窗口中右键单击以访问多个选项,例如在图 9.15中提到的包含自定义网格。

图 9.15:着色器图中的主预览及其选项截图
在将节点连接到主堆栈之前,此预览将默认为灰色球体。接下来,让我们谈谈节点!
节点
Figure 9.6 to play with the gradient yourself.

图 9.16:节点创建菜单
如果您花了一些时间来构建这个内容,您可能也打开了一些节点分组,并意识到有非常多的节点可供选择。这可能会引起一些焦虑。幸运的是,在下一节中,我们将介绍一些在许多着色器中常用的节点。
常用节点
以下是一个用于制作着色器的常用节点简单列表。我们想强调,这并不是节点的完整列表。实际上,在 Shader Graph 10+中现在有超过 200 个节点。详细地介绍它们可能相当于一本书或两本书的内容。这些节点集的有趣之处在于,它们可以被构建起来制作一些令人难以置信的着色器。在阅读这些内容时,请注意,前一个节点中可能包含有助于描述当前您正在阅读的节点的信息。即使您对如何添加等操作相当熟悉,也请通读所有内容。
添加
为了能够解释添加,我们需要确保您记得 0 代表缺席,或者在这个情况下是黑色。这意味着 1 的值是白色。我们在许多应用中在 0-1 的尺度上归一化我们的值。您可能记得 UV 坐标也是在 0 和 1 之间。这不是错误!所以,如果我有两个标量,或者Vector1s,并将它们相加,值会更大。
让我们通过一个快速示例来展示:0.4 + 0.4 = 0.8。

图 9.17:添加节点
0.4 是一个比中等灰色更暗的值。如果我们把它们加在一起,我们几乎可以得到白色!0.8 在值上是 80%的纯白色,如图图 9.17所示。
颜色
这个节点是一个带有良好视觉糖的 Vector4。Vector4 (0, 0, 0, 0) 在着色器值中代表红色、绿色、蓝色和 Alpha。颜色提供了一个界面供您选择所需的颜色,并且它会为您设置 RGB 值,同时使用 Alpha 滑块设置该值,并输出一个 Vector4\。

图 9.18:颜色节点
使用 Vector4 节点来做这个可能会很困难,因为没有颜色视觉来知道您的值需要是什么。
Lerp
Lerp代表线性插值。Lerp 节点可以在许多应用中使用。一个例子是如何在图 9.6中设置用于基础颜色的渐变。它有三个输入:A、B 和 T。您可以将 A 视为 0,B 视为 1,T 是驱动器。驱动器(T)是一个 0-1 之间的值。然而,这个值将映射到 A、B 以及它们之间的值。如果 T 为 0,它将显示 A 的 100%值。如果 T 的值为 1,它将显示 B 的 100%值。现在,如果 T 为 0.4,那么它将在 A 和 B 的值之间:40%的 A 和 60%的 B。

图 9.19:Lerp 节点
仅用数字很难可视化这一点。幸运的是,在 图 9.19 中,我们使用了 UV 将 T 作为渐变输入。这使我们能够看到从底部到顶部的 0-1 渐变。你看到的是从 A 到 B 的渐变,从底部到顶部。
乘法
我们已经看到了 添加 和 线性插值 节点;现在我们需要处理另一个操作,乘法。根据基本算术的性质,乘法会使值降低。
这是因为我们处于 0-1 的范围内。让我们在 图 9.20 中放一个例子。

图 9.20:乘法节点
我们使用了与添加节点相同的示例,但我们使用乘法而不是加法。简单的数学运算表明 .4 * .4 = .16。
样本纹理 2D
这个节点允许你使用在其他 数字内容创作 (DCC) 软件中创建的纹理,例如 Photoshop,并使用颜色信息来操纵主堆栈的属性。有两个输入,你想要采样的纹理和 UVs,如 图 9.21 所示。

图 9.21:样本纹理 2D
UV 节点允许你操纵网格的 UV 坐标。这个节点的优点是它不仅输出 Vector4,还从节点本身输出每个浮点数。
饱和
有时候,你的值可能会超过 1。这可能是由于你正在处理多个节点,这些节点将你的值推到了 0-1 范围之外。

图 9.22:饱和节点
如果发生这种情况,你可以将数据输入到一个饱和节点中,它将返回所有值在 0-1 范围内。将浮点值放在 输入 部分,输出 值将被归一化到 0-1 范围。
分割
正如我们在 样本纹理 2D 节点 中看到的,Vector4 被分割成单独的输出。这并不总是这种情况。颜色节点只输出 Vector4。如果你只想从 颜色 中使用红色通道值,你该如何获取它?你猜对了,使用一个分割节点。输入一个 Vector2、3 或 4,并使用你想要的任何通道作为浮点数。

图 9.23:分割节点
这对于能够从你放置了四个灰度图像的图像中提取出来非常有帮助。我们称之为 通道打包,这样你就可以在一个纹理查找中放置三幅图像。
UV
有时候你需要操纵你想要渲染的对象的 UVs。一个原因可能是因为你想要平铺 UVs,因为项目的比例比预期的要大或小。使用 UV 节点的另一个原因是它会自动创建水平和垂直渐变。

图 9.24:UV 节点
如果分割,R 通道是水平渐变,G 通道是垂直渐变。
向量
这些节点无处不在。你会注意到 Vector1 被命名为Float。Vector1 的另一个名字是 Scalar。你可能还注意到输出都是不同的颜色。Float 是青色,Vector2 是绿色,Vector3 是黄色,而 Vector4 是粉色。了解这些颜色在节点之间的连接线上是如何显示的非常有用。这些节点在无限的使用案例中都有应用。你需要三个数据点吗?Vector3!

图 9.25:向量节点
使用所有这些基本节点,你可以制作一些强大的着色器,用它们来制作美丽的材质。在第十二章,收尾工作中,我们将介绍用于多种目的的着色器,并展示我们如何使用这些节点来创建一些漂亮的视觉效果。现在让我们从着色器图离开,开始处理粒子系统,以便我们可以为我们的体验添加一些漂亮的视觉效果。
粒子系统
当你想到视频游戏中的视觉效果时,最可能首先出现在你脑海中的可能是 ARPG 中传奇武器的尾迹或来自紧张的第一人称射击战役中的惊人爆炸。无论你脑海中浮现的是什么,都有系统可以允许这些效果发生。粒子系统允许根据特定规则生成网格,以创建这些效果。Unity 中有两个系统是 Shuriken 和 VFX 图。
Shuriken
这个系统充满了帮助生成 3D 网格(定义 3D 对象的顶点、边和面的结构集合)的功能。你可以创建火花、尾迹、爆炸、烟雾以及所有其他有助于增强所定义体验的东西。正如你在图 9.26中看到的,有很多选项需要探讨。我们将把这个解释留给第十二章,收尾工作中创建基于 Shuriken 的效果时涵盖的例子。
一些关于 Shuriken 的高级知识是,它是一个使用 CPU 来引导粒子的粒子系统。
这限制了可以在该硬件上直接生成粒子的数量。

图 9.26:Shuriken 粒子系统
Shuriken 对于 CPU 上的粒子系统非常出色,但如果你想要有大量粒子四处移动,VFX 图就是最佳选择。这可以驱动 GPU 粒子,并且可以同时处理成千上万的粒子。
VFX 图
首先,您很可能会需要安装 VFX 图。像之前一样打开包管理器,然后在Unity 注册表中找到视觉效果图并安装它!完成此操作后,您需要创建一个 VFX 图系统。在您的项目窗口中右键单击,然后选择创建 > 视觉效果 > 视觉效果图。

图 9.27:安装和创建您的第一个 VFX 图系统
打开 VFX 图将为您显示一个新窗口。这个窗口就像着色器图。您会注意到有一个黑板,我们可以用它来创建可以在运行时更改的参数,并在检查器中公开。
有一个独特的 UI 和几个特定术语:上下文、块、节点和变量。上下文是将系统分解的部分,例如生成、初始化、更新和输出。
这些上下文中每个都包含块,可以通过右键单击将它们添加到上下文中。生成****上下文控制有多少个系统的实例输入到初始化****上下文。初始化****上下文处理生成事件并启动一个新的粒子模拟。更新****上下文接收已初始化的粒子并在某些条件下执行显式行为。输出****上下文负责模拟数据,并根据输出****上下文配置渲染每个活着的粒子。然而,输出****上下文不会修改模拟数据。
第一个上下文是生成。这个上下文将允许您添加块来影响每个系统的生成逻辑。这里有一些应该考虑的问题:从这个系统中应该生成多少个粒子?这些粒子应该多快生成?它们何时生成?

图 9.28:生成上下文
生成完成后,您需要有一些参数来初始化它们。这些块回答这些问题:粒子在哪里生成?它们是移动生成还是有速度?每个粒子能存活多久?

图 9.29:初始化上下文
现在您已经让它们开始生成,可能有必要在它们更新时添加一些独特的行为,否则它们将只是漂浮的球面渐变。您可以期待这回答一个终极问题:粒子将如何随时间变化?

图 9.30:更新上下文
最后,当您了解了有多少粒子,粒子在哪里,以及粒子在做什么后,您现在可以决定它们的看起来会是什么样子。
问题是:粒子面向哪个方向?使用了哪种着色器?

图 9.31:输出上下文
你可能已经注意到,块状图左侧有时会有类似着色器图的圆形输入。如果你认为你可以将这些输入放入其中,也许是从节点来的,那么你是对的!有一些节点你可以通过它们来获取正确流向每个上下文块的数据。
节点
由于这是在着色器图中,节点的作用是获取数据值并以某种方式操作它们,以获得期望的结果。在 VFX 图中,值被用来读入其中一个块,而不是从主栈中的一个属性。在大多数情况下,你将利用运算符节点和在黑板中创建的变量来完成复杂的数学运算。
摘要
我们通过 Unity 中的两个主要来源了解到视觉效果有重大的技术影响:着色器和粒子。我们花时间在着色器上,构建了一个 3D 对象上的材质示例,以便在有多种不同场景时,我们可以追踪着色器的创建过程。这是通过着色器图完成的。之后,我们深入探讨了粒子的概念。Shuriken 被用来获得对 CPU 粒子的简单理解,将在后面的章节中用来解释细节。GPU 粒子是通过 VFX 图创建的;我们讨论了 VFX 图的界面和一些词汇,以便在以后使用时,有一个理解来工作。
视觉效果是一个非常大的主题,要掌握它需要很长时间。当你处理这个主题时,请慢慢来,快速失败。这将帮助你理解视觉效果。
下一章将介绍游戏中声音的实现。声音通常在游戏快结束时才被注意到,但它们对于确保产品与环境和角色有吸引人的情感联系是至关重要的。下一章我们将讨论实现、一些声音设计和其他与声音相关的知识点。
第十章:声音效果
声音效果!声音是唯一来自真实世界并进入游戏的部分。使用麦克风,声音设计师将录制常见的视频游戏声音,如配音、音乐、UI 声音、武器和环境声音,以帮助使游戏栩栩如生!声音往往对玩家对视频游戏质量的感知有非常微妙的影响。游戏中看起来不错的动画,其声音效果也只能与其声音一样好。
在本章中,我们将讨论选择或设计声音的五个要素。这些是源、包络、音高、频率和分层。理解这五个要素将为你提供一个坚实的基础,以确保你的声音适合我们迄今为止所工作的叙事、角色、环境和机制的整体设计。然后我们将讨论如何在游戏引擎中通过我们的代码和混音来扩展这些元素!声音在单个声音效果的基础上讲述故事,同时也共同讲述一个更大、更深入的故事。最后,我们将通过我们项目中的具体声音设计示例以及它们在 Unity 中的实现来探讨。这些示例包括魔法声音、脚步声和环境声音。以下是本章的简要总结:
-
声音设计的五个要素
-
规模化设计
-
我们项目的声音设计和实现
-
通过玩家交互触发声音
声音…设计?
声音设计!它是视频游戏中被遗忘的次子,但也是它们的灵魂和情感。
声音设计的简单解释是,声音被录制、处理,然后直接编码到游戏中。因此,这使得声音成为视频游戏中唯一直接来自真实世界的部分。
本章中提到的任何声音都可以在 /Assets/Sounds/[Name] 中找到。
声音设计的五个要素
我们将要讨论的声音设计要素包括源、包络、音高、频率和分层。这些适用于制作单个声音效果的过程,以及声音在游戏中的更广泛作用。
源
源可以是一个人、一个地方或一个事物,它是你灵感的来源或获取途径。你的源是帮助你的听众理解你声音的真实世界特性的东西。如果你录制了脚步声击打草地表面和混凝土表面的声音,这两种声音之间的细微品质和差异帮助我们区分它们。因此,我们可以利用源作为创造我们声音的真实性的创造性限制。
限制是艺术家用来从我们的大脑中剔除所有杂乱,以帮助创造他们愿景的工具。因此,在我们的录音过程中,如果我们需要在视频游戏中录制神奇的水声,我们会先录制一些水声作为基础层。或者,如果我们想要录制一只狗在泥地里打滚的动画声音,首先和最好的录制内容就是狗在泥地里打滚的声音。我们正在为哪些声音创作帮助我们选择要录制的内容!
录制声音可能是一个困难的过程,它背后有着完整的艺术形式;虽然它将有助于你作为声音设计师的成长,但我强烈建议使用现有的声音库。几乎你能想到的任何声音都已经有人录制过了,所以直接在网上购买或下载声音更有意义!这将极大地加快你的工作流程。如果你不想使用声音库,那么你可以使用麦克风!使用麦克风是一个非常深入的过程,我们在这本书中不会涉及,因为你可以真正地写整本书来介绍这门艺术。
这里有一些流行的免费网站和声音库:
-
Blipsounds:
blipsounds.com/community-library/ -
Andrew V Scott:
www.andrewvscott.com/Building-A-Large-SFX-Library-for-Free -
SKYES Audio:
www.skyesaudio.com/blog/2019/4/1/the-ultimate-free-sound-effects-list-free-to-download -
Freesound:
freesound.org/
通过一些网络搜索,你还可以找到更多。不要害怕四处寻找适合你需求的东西。
在视频游戏中,声音效果来源通常由你视觉上看到的内容决定。对于一个施放冰风法术的冰法师,你会限制自己只使用风和冰的声音来开始。如果你有一把由果冻制成的枪,它用放射性屁的力量射出小马,你可能会利用果冻、马、放屁和枪的声音。
那么,如果我们拿一把带有金属纹理的魔法剑,以及沿着剑刃运行的紫色魔法 VFX 效果,我们将寻找什么样的声音呢?你可能已经想到,我们将使用魔法声音库和一些金属的叮当声。
另一种确定声音来源的方法是通过故事背景。同样的魔法剑可能看起来很神奇,但也许游戏的编剧决定剑使用的是未来魔法,因此你需要使用科幻声音元素。
应该提到的是,有许多游戏在声音上有限制,必须填补这些空白。一个模仿 Atari 2600 图形并具有真实声音设计的游戏可能需要一些想象力。玩家行走的绿色区域可能是草地或有毒垃圾场,这取决于游戏世界的背景。
Envelopes
包络是声音设计师用来解释声音随时间变化的音量的方式(这里的音量是指分贝,而不是 3D 模型)。你将使用的包络的两个部分是“攻击”和“释放”。
攻击,如图 10.1 所示,是声音的开始,释放是声音的结束。我们通过速度(即快和慢)来描述声音的攻击和释放。

图 10.1:包络解释
攻击
具有慢攻击的声音的例子可以是当剑在空中挥舞时产生的“剑啸”声效果。声音一开始几乎听不见,在半秒内逐渐增加音量。我们可以通过让音量在几秒内增加来使攻击变得更慢。你可以在图 10.2中直接看到包络在波形中的样子。具有较慢攻击的声音往往对玩家来说显得更微妙和温柔。其他一些具有慢攻击声音的例子包括汽车经过或水壶准备鸣哨。

图 10.2:快速攻击声音
类似于图 10.2中快速攻击的例子,可以是击打声效果。声音一响起,几乎就是最大音量,创造出瞬态声音。瞬态声音是一种攻击速度较快的声音,对玩家来说似乎更具侵略性,通常用来传达力量或震惊玩家。其他一些具有快速攻击声音的例子包括枪声、如图 10.3 所示的钹声,或者锤子击打铁砧的声音。

图 10.3:快速攻击的钹声示例
释放
然后是我们有声音的释放。正如你可能猜到的,我们将使用速度来确定释放的性质。具有较慢释放的声音的例子可以是汽车引擎关闭或爆炸声。大多数音效都将具有较慢的释放,因为它听起来更吸引人。
在视频游戏中,你将听到的具有短释放的例子并不多。在音效中,硬截止通常在大多数情况下听起来不自然且令人不快,除非是一些高级的样式化技巧。具有慢释放的声音可以是巨大的钟声响起,或者汽车驶入远处的声音,同时你听到声音的音量逐渐消失。为了举例说明,这里是一个慢释放的例子:

图 10.4:慢释放声音
这里是一个快释放的例子:

图 10.5:快释放声音
声音的另一个元素是音高。
音高
音高是决定声音“高低”的元素。
这可能是我们通过电影、视频游戏甚至日常生活中的体验,最容易理解的概念之一。在动画电影中,一个高大健壮的角色通常会有低沉的声音,而一个较小、可爱的角色可能会有尖细的声音。
上面给出的例子是控制声音效果音调的最常见原因之一——大小。另一个原因是速度。想象一辆慢慢行驶的汽车和一辆快速行驶的汽车。行驶速度较快的汽车发动机转速较高,而闲置或缓慢移动的汽车则会发出低频的风箱共鸣声。
要完全理解音调,了解频率会有所帮助,因为它们直接相关。
频率
频率是解释起来最复杂,但也是理解起来最重要的元素之一。你可能已经在汽车或立体声音响中听过音乐,并看到过控制“低音”或“高音”的选项。高音指的是“较高频率”,而低音指的是“较低频率”。人类的听觉范围是 20 Hz 到 20,000 Hz(赫兹),许多声音,无论听起来是高音还是低音,都会触及每一个频率。当你在你车里播放声音并降低“低音”时,你实际上是在降低较低频率。
最好的例子是白噪声。白噪声简单地说就是以相同音量播放每个频率的声音。如果你从未听过它,它听起来就像电视静电。你可以在/Assets/Sounds/WhiteNoise.wav中听到这个声音。你可以在 GitHub 上的项目中找到它,这本书的前言中有一个链接。
这个声音奇怪的地方在于,仅仅通过听,它感觉主要由高频组成。但我们可以使用一个叫做均衡器(或简称 EQ)的工具来可视化正在播放的频率,以及控制单个频率的音量。
通常,较高的频率会被感知为较响亮,这在为你的游戏制作声音时是一个重要的考虑因素。如果你想使某个声音突出,包括较高的频率会有很大帮助,而去除它们可能会帮助将声音融入背景。但如果你想让每个声音都突出,同时还想让它们有重量和力量,我们就必须利用我们的低频。因此,必须找到一个平衡点。
这张图上的黄色线条表示频率的冲击点,你可以看到在整个频谱上几乎处于相同的音量。这意味着每个频率通常具有相同的音量。

图 10.6:相似频率音量示例
我提供了一些声音,这样你们在移除高低频时可以听到差异,同时还有一个图表显示我们移除了哪些频率。当你听的时候,你会听到并看到每个声音似乎都覆盖了一定程度的高低频,我们可以控制这些频率,以每个声音唤起独特的感受。
听一下Assets/Sounds/Explosion.wav然后是Assets/Sounds/ExplosionLP.wav,以了解高频被截断后的声音效果。然后听一下ExplosionHP.wav,以了解低频被截断后的声音效果。
听一下Assets/Sounds/BeamSword.wav然后是Assets/Sounds/BeamSwordLP.wav,以了解高频被截断后的声音效果。然后听一下BeamSwordHP.wav,以了解低频被截断后的声音效果。
听一下Assets/Sounds/MagicIceSpell.wav然后是Assets/Sounds/MagicIceSpellLP.wav,以了解高频被截断后的声音效果。然后听一下MagicIceSpellHP.wav,以了解低频被截断后的声音效果。
听一下Assets/Sounds/Footstep.wav然后是Assets/Sounds/FootstepLP.wav,以了解高频被截断后的声音效果。然后听一下Assets/Sounds/FootstepHP.wav,以了解低频被截断后的声音效果。
频率之所以是最难掌握的概念之一,是因为你的耳朵没有经过训练去聆听它。我们只是听到一个声音,就知道它听起来好不好。
层次
层次是五个元素中最容易理解的概念。虽然视觉媒介几乎都是乘法关系,但声音媒介是加法关系。层次简单来说就是以独特的顺序同时播放多个声音的过程。接下来,我们有四个独特的音效,它们各自独立。我们有“冲击”、“绽放”、“尾音”和“低音”。如果你单独听每个音效,它们会感觉空洞,但当我们把它们全部加在一起时,我们就得到了一个美丽的爆炸声。
这是一个有用的过程,因为我们可以从“科幻能量”和“金属剑”这样的源文件中提取,将它们结合起来制作一个“科幻能量剑”。或者,我们可以拿我们的马屁枪,根据枪的描述和功能来选择我们的层次。听一下Assets/Sounds/ScifiEnergySword01.wav,然后是Assets/Sounds/ScifiEnergySword02.wav,然后是Assets/Sounds/ScifiEnergySword03.wav。
层次还允许我们将频率分解成更独立的部分。我们可以将主要包含低频的声音添加到常规声音中,以增加它的重量和力量。听一下Assets/Sounds/Splash01.wav,然后听一下添加到Assets/Sounds/Splash02.wav中的低频,以了解它如何变得更强大。
我们还可以将两个具有不同包络的音效叠加在一起,一个具有较长的攻击时间,另一个具有较快的攻击时间,以创建一个酷炫的累积效果,增加冲击力。在添加累积效果之前,请听一下Assets/Sounds/EarthSpell01.wav中的声音,然后听一下Assets/Sounds/EarthSpell02.wav,以了解添加累积效果后会发生什么,这样我们可以了解如何改变我们声音的故事!
现在我们已经了解了构成声音的要素以及它们如何应用于单个音效的创建,接下来我们将探讨这些五个要素在游戏更广泛的应用。
规模化设计
与艺术不同,制作音效是一个完全累加的过程。例如,如果我们有 100 个声音,但没有注意它们的音量或频率范围,这可能会导致很多杂音。在视频游戏的空间中,我们必须为任何声音做好准备,无论是剑挥舞声、环境音、管弦乐或旁白,都要同时播放。我们有工具可以单独控制这些声音,但我们必须确保我们有一个所谓的平衡混音。
如何为游戏制作声音
那么声音在你的游戏中应该放在哪里呢?声音往往会被忽视,因为从技术上讲,它们不是制作“游戏”所必需的。正因为如此,立即考虑哪些需要声音,哪些不需要是很困难的。
简单来说,我喜欢寻找游戏中任何移动的元素。甚至包括最小的细微之处。为 NPC 添加声音可以包括呼吸声、他们的脚步声击打地面的声音、衣服的沙沙声... 所有这些都可以是游戏中可行的声音。我们将在第十一章中讨论一些原因,说明为什么如此关注细节可能很难实现。
有时游戏中的艺术表现是简约的。你所看到的信息并不足以提供足够的信息,因此请发挥你的想象力,思考可能包含哪些声音!你添加的声音越多,效果越好。有时一个游戏可能仅仅是像素艺术,你可能会被鼓励添加较少的声音,但你应该始终思考你能听到的细节,而这些细节是看不到的!如果玩家掉入你无法看到的地方,创造一个可听到的体验可以讲述比视觉更详细的故事!也许有尖刺、无底洞或熔岩!我们希望玩家能听到熔岩的气泡声,尖刺刺穿玩家的冲击声,或者玩家从陡峭的山崖上掉落时的尖叫声!
我们项目的声音设计和实现
我们发现,最好的学习方法是全力以赴地投入其中,开始理解事物是如何运作的。现在我们已经了解了 Unity 引擎,这个过程相对简单。
播放第一个声音
首先,让我们将一些音频文件放入我们的项目中。我们将在 Unity 中创建一个名为Audio的文件夹,然后将Assets/Sounds/TestTone.wav文件拖入其中。
现在我们已经将音频文件放在了文件夹中,让我们在场景中为玩家创建一个空的 GameObject。我们将首先在场景中放置一个与我们的角色相邻的对象。暂时让我们称这个 GameObject 为Sound Emitter。
按照现状,这个 GameObject 不会做任何事情。所以,让我们点击并拖动我们的音频文件从其 Unity 文件夹直接到Sound EmitterGameObject 的 inspector 中。
这将在 GameObject 上自动创建一个Audio Source组件。这是允许我们在 Unity 中播放声音的组件!让我们继续并点击播放来查看会发生什么!
如果你正确地操作了,你很可能已经听到了你的第一个声音!恭喜!当然,这只是一个占位符声音,所以我们将更多地关注添加游戏中将使用的其他声音。在这个过程中,我们将讨论可以在Audio Source组件上更改的参数。
组织
为了更好地组织这个项目,让我们继续添加一个新的 prefab,名为====SFX====。我们将把场景中存在的所有音效放入这里。
除了这个之外,我们将在我们的 GitHub 项目中在/Assets/Sounds/目录下创建两个新的文件夹。我们将有一个sources文件夹和一个prefabs文件夹。
音乐
音乐是电子游戏的重要组成部分。音效通过详细的环境声音、玩家声音和表达性的旁白帮助游戏栩栩如生,但音乐是帮助推动玩家每一刻情绪的关键。
你可能对声音和音乐之间的区别有一些疑问。技术上它们是相同的,但为了更容易地交流,大多数专业音效设计师会将音乐视为配乐,或者是有音高的乐器,如钢琴、小提琴、吉他和大鼓,它们一起创作出一个连贯的歌曲或音乐轨道,作为背景音乐。与此同时,音效通常是现实生活中的实例,如脚步声、剑声、UI 声音等。
让我们在游戏中添加音乐!要添加音乐到我们的游戏,我们只需要将场景中的SFXGameObject 重命名为Music。
让我们听听Assets/Sounds/Music_01.wav。首先,在文件夹中选择音频文件,然后在 inspector 底部的循环按钮上点击。这是在下面的图 10.7中看到的,位于波形右侧的按钮上方。

图 10.7:inspector 中的波形播放 UI
现在,点击播放按钮,这是 inspector 中向右的侧向三角形,但在循环按钮的左侧。
如果你一直听到音乐结束,你会意识到音乐是一个无缝循环!为了在游戏中听到这个效果,让我们将场景中的Sound EmitterGameObject 重命名为Music。
接下来,让我们点击并拖动我们的音乐到新的Music游戏对象中。我们将将其放入图 10.8中看到的Audio Source组件的AudioClip空间。

图 10.8:Audio Source 组件
如果我们现在测试玩游戏,我们能够听到声音,但最终它会停止。但我们按下了检查器中.wav文件的循环按钮,对吧?那么为什么它不起作用呢?
好吧,那个循环按钮只是为了在那个特定实例中播放。如果我们想在Audio Source组件上循环声音,我们必须在图 10.8中的Play On Awake下检查Loop。现在如果我们玩游戏,我们的音乐将会循环!多么令人兴奋!
在本章的后面部分,我们将调整游戏中所有声音的音量,这通常被称为“混音”或“游戏混音”。我们不会立即混音游戏,因为游戏中每个声音的音量完全取决于它相对于其他音效的声音。
2D 声音
到目前为止,我们只听到了 2D 声音效果。2D 声音效果是没有游戏位置的声音,将在任何地方为玩家播放。无论你在地图上移动到哪里,2D 声音都会作为一个一致的触发器播放。
以下是在玩电子游戏时可能听到的 2D 声音列表:
-
音乐:在按下播放按钮之前,在开场菜单中播放的介绍音乐
-
用户界面(UI):按下按钮时听到的“咔哒”声
-
旁白:在游戏过程中说话的解说员
-
环境声音:在某个区域播放的通用声音,例如风声
上述所有类别都可以通过玩家操作、游戏事件、开始游戏或进入游戏的新区域来触发。但并非所有这些都会存在于游戏中的 3D 空间中。这使得它们成为 2D 声音效果。
因此,既然我们已经讨论了 2D 声音是什么,那么让我们来谈谈 3D 声音。
3D 声音
与 2D 声音不同,3D 声音存在于游戏的世界中。当你玩游戏时,你可以通过在世界中移动并听到哪些声音在哪个耳朵中发生来判断哪些声音是 3D 的。这被称为横滚。
横滚是指声音的立体声质量。你有没有在戴耳机听歌时,只摘下一只耳朵,结果听到的是部分乐器演奏而不是整首歌?这就是横滚的原理!制作那首歌的音乐制作人故意将那些乐器放在一只耳朵里,以创造更好的“立体声成像”(我保证不会深入探讨这个话题)。
因此,在现实世界中,如果有人在你的左边说话,你会在左耳听到他们,而右耳听到的声音会少一些。我们希望在电子游戏中重现这种感觉。因此,我们将位置声音定义为 3D 声音。
使用 3D 声音
让我们做一个实验。让我们将MusicGameObject 的Spatial Blend选项从0调整到1。
现在我们有了空间音频!这意味着我们的声音将在 3D 中!
就目前而言,可能很难确切地指出音乐是从哪里播放的,因为没有视觉指示器。所以,为了解决这个问题,我喜欢创建一个作为音频源子对象的球体 GameObject 来可视化它确切的位置!

图 10.9:将球体 GameObject 设置为子对象
现在我们点击播放,我们可以确切地看到音频源是从哪里播放的!接下来,我们将讨论如何控制我们的 3D 音效参数。
音频监听器第一部分
我们如何在游戏中听到声音?我们是通过音频监听器来听到声音的。这是一个我们可以放置在任何 GameObject 上的组件,它充当一对虚拟的耳朵。在大多数情况下,放置这个监听器非常简单,但有时我们需要更复杂的集成。
在使用第一人称相机的游戏中,这很简单:我们只需将监听器添加到相机 GameObject 中,然后就算完成了。
我们在相机上使用音频监听器,因为它充当玩家的耳朵。但有时相机可能处于等距视图,并且相机离玩家太远,无法正确地平移并听到随着它们在世界中移动的声音,因此我们将在一个新的 GameObject 上偏移音频监听器,使其与相机偏移。
我们将在音频监听器第二部分中回到这个问题。现在,让我们设置一些 3D 音效设置。在我们设置好 3D 声音之前,我们无法在实际中利用音频监听器。
3D 音效设置
当你在现实生活中听到声音时,你通常可以靠近它,它就会变得更响,而当你远离它时,它就会变得 quieter,最终变得无声。我们可以通过在音频源组件上的 3D 音效设置来控制这种效果。
我们将专注于 3D 音效设置中的最小距离和最大距离。
在音频源组件中,将最大距离改为 10,然后点击播放。假设你还在你的 GameObject 上保留着球体,在游戏中靠近它再远离它。为了进一步可视化,让我们在 Unity 中将场景标签页取消停靠,并将其与我们的游戏标签页并排放置。
现在我们已经做了这些,我们可以在游戏中使用线框球体工具来可视化最小和最大距离!我们可以看到,当我们把玩家移动到球体范围之外时,我们将不再听到声音。
使用我们的最大距离滑块,我们可以控制我们能听到声音的距离。而使用最小距离,我们可以控制声音最响亮的位置。让我们将最小距离改为 3。你会注意到,在较大球体内部的较小球体会随之改变,如图 10.17 中所示。
当我们将玩家移动到这个球体内部时,你会注意到没有声像。这是因为声音已经达到最大音量,在较小的球体内部,声音将变成 2D 声音!
最后,我们只想将音量衰减设置为线性衰减,而不是对数衰减。我们这样做的原因是,当你在对数衰减模式下将最大距离更改为小于 500 的数字时,声音实际上并不会在那个距离被切断。所以如果我们把最大距离设置为 10,即使我们在地图上 400 个单位的位置,我们仍然会听到它,尽管我们设置的最大距离远小于这个值。
为了参考,这里是对数衰减的对数:

图 10.10:对数衰减
这里是线性:

图 10.11:线性衰减
音频监听器第二部分
你可能已经注意到,当你的玩家在球体内部时,音频感觉有点不对劲。通常,当我们的玩家穿过球体时,它并不是最响的;它只有在摄像机靠近球体时才达到最响。这是因为 Unity 的音频监听器默认设置为在摄像机上。
在一个第三人称游戏中,就像我们正在制作的,我们希望将其添加到玩家身上,但有一个问题。我们希望它在玩家身上,而不随玩家旋转。我们希望它随摄像机旋转,如下面的图 10.12所示进行选择。

图 10.12:在层次结构中选择摄像机
如果我们打开我们的场景,我们可以看到MyvariWithCameraRig已经附加了Main Camera。在检查器中,我们会找到一个名为音频监听器的组件,如下面的图 10.13所示。

图 10.13:检查器中的主摄像机上的音频监听器
现在,作为一个实验,让我们在这里移除音频监听器,并将其直接移动到我们的主要角色上。只需将其放置在Character游戏对象上即可。
现在玩游戏,并在球体对象周围移动和远离,旋转它。你会注意到声像到处都是!从我们的视角观察角色,很难判断信息来自哪里,因为我们不是站在角色的位置;我们有一个第三人称视角。
在这样的游戏中,我们可能只需要将我们的音频监听器放置在摄像机上,但将它放在我们的角色模型上会很有帮助。但我们不能这样做,因为玩家没有被锁定在其旋转上。
但有一个解决方案!在大多数游戏中,我们可能需要将其作为子游戏对象添加到MyvariWithCameraRig游戏对象内部的Main Camera中。但在这里,我们已经做了大部分工作,因为根MyvariWithCameraRig变换已经与角色模型对齐了!
我们必须做的就是在一个根MyvariWithCameraRig内部创建一个新的游戏对象,将其重命名为Listener,如下面的图 10.14所示,然后我们可以向其添加音频监听器组件。

图 10.14:放置音频监听器的新游戏对象
接下来,我们可以将这个Listener游戏对象沿着y轴向上移动,使其正好位于我们角色的耳朵旁边,如下面的图 10.15中通过变换小工具看到的。

图 10.15:音频监听器游戏对象在 Myvari 头部高度对齐
我将其在y轴上向上移动了 1.5 个单位。现在当我们移动时,Listener游戏对象的transform将随着相机移动。我们的 3D 声音现在将相对于角色的位置播放,而不是相对于相机!
将 3D 环境声音添加到游戏中
在你的生活中,你有多少次经历过绝对的寂静?你可能认为在客厅里享受一个安静的夜晚就是绝对的寂静,但你仍然能听到空调、冰箱运行、窗户外的声音等。
当然,提到的这些声音非常微弱,但重点是,我们从未真正体验过绝对的寂静!
因此,即使在我们的视频游戏中,如果玩家处于空闲状态,没有移动,并且完全静止,那么始终有一些声音会很有帮助。这就是环境声音发挥作用的地方。
环境声音可以一般定义为“存在于 3D 空间中的声音,但不会移动。”在我们的 Holy Forest 项目中,我们可以添加树木沙沙声、洞穴内部、传送门嗡嗡声、从物体发出的魔法能量、河流等等的声音!
添加环境声音相当简单。实际上,我们已经在技术上做了这件事!我们在3D 声音设置部分听到的声音在技术上可以被视为环境声音。
让我们从场景中非常简单的树木沙沙声环境音效开始。
让我们将Assets/Sounds/AMB_Trees3D.wav文件拖放到一个游戏对象的音频源组件上。我们将音量衰减设置为线性衰减,并将空间混合设置为 1。接下来,我们将最小距离设置为 1,最大距离设置为 5。
完成这些后,我们可以将我们的游戏对象的变换值设置为以下图像所示。图像中的变换反映了图 10.16中看到的背景声音游戏对象,并且在场景中的声音部分,在AMB_Trees3D第一个游戏对象的层次结构中是物理存在的。

图 10.16:环境树 3D 声音转换
我们将把它放在玩家出生点左侧的大树上。在下面的图像中,你可以看到我们的声音小工具放置在场景中。
你也可以在下面的图 10.17以及场景中看到这一点。在层次结构中双击AMB_Trees3D游戏对象,你将物理地被带到场景中的那个位置。

图 10.17:环境音频源的小工具
最后,我们只想确保唤醒时播放被勾选,这样声音在场景开始时就会立即播放,如图图 10.18所示。

图 10.18:确保“唤醒时播放”设置为 True
现在我们按播放。在这里,我们将看到声音在游戏中正常播放!它将与我们之前的声音一样工作,我们可以听到它的方向性,当我们离开树的半径时,声音最终会消失!
填充我们的环境声音
对于剩余的环境声音,它将是我们刚刚所做事情的重复。我们将包括环境声音的最小/最大范围和位置,以及我们认为适合每个环境项目的音频文件。在场景中,我们将环境声音设置在====AMB====下,如图图 10.19所示。我强烈建议你听听环境声音,看看它们听起来如何!

图 10.19:层次结构中环境声音的列表
2D 环境声音
如果你在我们刚刚填充的场景中四处走动,你会注意到它现在感觉更加生动了!然而,你会在某些地方注意到 silence,正如我们所学的,听到绝对的 silence 绝不是我们希望玩家在游戏中体验的事情!
让我们在====SOUND====父游戏对象中添加一个音频源,并扔进我们的General2D_Amb.wav。
通过玩家交互触发声音
我们迄今为止创建的所有声音都是在进入场景时立即播放的声音。这是因为我们在音频源组件中勾选了唤醒时播放。
如果我们没有勾选这个选项,声音将永远不会播放。但酷的是,我们可以以其他方式触发声音!
通过 Unity 事件触发声音
让我们为我们的第一个楼梯谜题获取一个声音。这个将会相当简单。我们添加声音的最简单方法是将音频源组件直接添加到触发区域游戏对象中。让我们找到LeftStairsTrigger,在检查器中向下滚动,直到我们找到交互触发脚本,如图图 10.20所示。

图 10.20:LeftStairsTrigger GameObject 上的交互触发脚本
如果你还记得,我们创建了一个名为OnInteract的UnityEvent,我们可以利用我们的Audio Source组件!继续在检查器底部点击添加组件并选择Audio Source。
接下来,将StairsPuzzleSuccess.wav文件拖放到Audio Source组件中。我们将保持Audio Source为 2D,因为我们播放的声音是一个奖励铃声。
现在,点击OnInteract UnityEvent中的+,并将None (Object)字段中的Audio Source组件拖放进去,如图 10.21所示。

图 10.21:添加到交互触发的声音
接下来,你会看到一个当前标记为No Function的下拉菜单。让我们点击它,然后下拉到AudioSource,然后到Play (),如下面的图 10.22所示。

图 10.22:在交互时向声音对象添加 Play 方法
这将确保我们在激活LeftStairsTrigger时播放音频文件。请继续点击播放并导航到LeftStairsTrigger。一旦这样做,你将听到我们的声音!让我们继续重复相同的步骤来处理RightStairsTrigger。
旋转拼图声音
第一次,我们将直接在代码中触发声音。这将是一个相当简单的过程,通过代码使我们的Audio Source变量公开可用。然后我们只需触发它。
我们将添加以下声音:
-
当拼图完成时播放的声音
-
当尖塔开始移动时的声音
让我们从最简单的一个开始,我们的“拼图完成”声音。当所有尖塔对齐并且门打开时,这个声音将会播放。进入场景中的First Puzzle预制体,打开FirstPuzzle.cs脚本。这个脚本是我们将添加代码的地方,如图 10.23所示。在第 173 行,继续输入以下内容:
public AudioSource puzzleCompleteSFX;

图 10.23:添加到第一个拼图脚本的公共 Audio Source
现在回到场景中的First Puzzle预制体,打开检查器,并添加一个Audio Source组件。在这个Audio Source上,我们将取消勾选Play on Awake并将FirstPuzzleJingle.wav拖放到它里面。
接下来,就像我们将音频组件拖放到UnityEvent中一样,我们将 Audio Source 拖放到新序列化的字段Puzzle Complete SFX上,如图 10.24所示。

图 10.24:将音频文件拖放到 Audio Source 组件中
现在我们最后一步是进入FirstPuzzle.cs脚本中的CheckForVictory()函数,进入第 241 行的if语句。在return true之前,在第 245 行,在图 10.25中,我们将添加以下内容:

图 10.25:向音频源添加播放功能
现在,让我们进入游戏看看它是否工作。当我们进入游戏时,我们应该能够激活我们的谜题,并在成功旋转尖塔时听到声音!
树形谜题
使用之前相同的方法,让我们添加一个当我们将球放在桥上、解决谜题的一部分以及完成最终谜题时播放的声音。我们将打开FinalPuzzle.cs并添加:
-
第 31 行上的
IntroPuzzleSolved.wav -
第 38 行上的
FinalPuzzlePartial.wav -
第 41 行上的
FinalPuzzleSolved.wav
概述
恭喜!我们刚刚迈出了理解游戏音频的第一步。我们已经了解了构成音效的要素,分解了声音设计的五个部分,了解了音频听众以及音乐与声音的区别,学习了如何使用 3D 声音,以及如何通过代码触发音频源组件!这是通过声音让我们的游戏充满活力的一个很好的开始。在第十二章,最后的润色中,当我们润色音频时,我们将介绍一些额外的技巧,让你的音频更进一步。
在下一章中,我们将继续构建我们的项目,这样你就可以与他人分享了。
第十一章:构建 & 测试
在开发旅程的这个阶段,我们已经一起完成了很多工作。现在,我们应该有一个游戏垂直切片,到目前为止,我们已经在 Unity 编辑器中能够玩到,并且它正在运行。这是很棒的,但你是否期望你的玩家下载 Unity 并打开包,然后在编辑器中玩游戏?我想不是!这就是我们想要将游戏项目构建成可执行文件的地方。在本章中,我们将介绍如何构建你的游戏,使其可以发布、测试,并最终让玩家能够玩到。
你将学习到:
-
从 Unity 构建
-
测试——功能测试、性能测试、游戏测试、浸泡测试和本地化测试
-
用户体验 (UX)
使用 Unity 构建
我们为了构建一个体验付出了很多努力。现在,我们需要能够将它带给人们。为了做到这一点,我们需要告诉 Unity 几件事情。它需要知道你正在为哪个平台构建,例如,哪些场景应该在应用程序中构建,哪个平台,以及影响构建输出可执行文件的其他选项。
在垂直切片的这个阶段,构建是一个很好的选择。在大多数项目中,这并不总是情况。在大多数情况下,与构建一起工作的最佳方式是:尽早构建,经常构建。在我们的情况下,我们需要等待我们有一些机制和两个主要谜题的标准游玩流程,然后我们才决定构建。
在图 11.1中,你可以看到构建设置菜单,它位于文件 > 构建设置下。在图片下方,我们将逐一解释这些设置。

图 11.1:构建设置
我们首先看到的是构建场景。由于它位于顶部,我们应该感觉到它很重要。这会自动将默认场景放入框中,但可能还有其他你想要的场景。你可能还有一个用于菜单系统的场景或另一个可能是教程级别的地图。这里的关键因素是要将你想要的场景放入这个框中;你可以直接将场景从项目窗口拖到构建场景框中。
列表顶部的场景将始终是第一个加载的场景。
在“构建场景”块下方,GUI 被分为两个部分,平台以及该平台的设置。在左侧,我们选择我们想要构建的平台。之后,该平台的设置将显示在右侧。我们将简要介绍PC、Mac 和 Linux 独立版选项。
如果你正在为任何其他平台构建,Unity 文档将帮助你了解构建过程。我们将描述大部分在下面可用的参数。控制台和移动设备的选择将有一些针对其目标平台需求的特定参数。
目标平台
这个选项很简单:你希望用这个构建针对哪个平台?这里的选项是 Windows、macOS 和 Linux。我们在这个垂直切片中为 Windows 构建应用程序。
架构
我们需要知道我们应该计划哪种 CPU 架构。32 位操作系统将要求你的游戏使用少于 4 GB 的 RAM。你可以这样做,但即使是小型游戏也可以使用 64 位;这对你的游戏没有坏处。一般来说,64 位应该是首选。
服务器构建
如果你正在开发多人游戏,Unity 可以为你创建一个游戏服务器。这将构建没有视觉元素的玩家设置。它还将构建为多人游戏定义的托管脚本。我们不会使用这个选项,但要知道这个选项是存在的。我们也不会用 Unity 讲解多人游戏开发,因为这将是一个从开始就完全不同的项目。
复制 PDB 文件
这是一个仅适用于 Windows 平台的设置。它将允许你在 Microsoft 程序数据库中进行构建以进行调试。我们也不会在我们的构建中使用这个选项。
创建 Visual Studio 解决方案
这也是一个仅适用于 Windows 平台的设置。启用此选项将允许你从 Visual Studio 而不是仅从构建设置菜单构建。如果你针对 macOS,则会有一个创建 Xcode 项目复选框。
开发构建
启用此选项将允许调试,包括分析器。分析器是一个分析器,用于了解在运行时正在执行什么操作。
我们将在本章的测试部分详细讲解这一点。这里还定义了一些将包含的设置。这对于当你需要测试你的应用程序并且担心性能时非常好。如果你有紧张的视觉预算,你尤其需要注意这一点。有一个术语叫做“基准测试”。这个术语指的是在目标机器上测试你的构建。如果你选择低端计算机进行测试,请注意其规格,并在开发模式下构建游戏,这样你可以在运行时运行分析器。一旦你有了基准测试,你就可以对它在高端和低端机器上的运行做出一些有根据的猜测。
自动连接分析器
如果你已经开启了开发构建,那么你可以启用此设置。它将自动连接我们在开发构建部分提到的分析器。
深度分析支持
这也要求启用开发构建。这将使 Unity 分析器能够记录更详细的数据。这不会是检查性能的最佳选项,因为执行脚本可能会有些减慢。深度分析构建的主要目的是通过记录所有函数调用来获取托管应用程序的具体原因。
由于每个方法都会单独记录,深度分析提供了对正在调用什么以及何时调用的非常清晰的视图。在游戏过程中,一些错误可以通过深度分析更容易地诱出和捕获。
脚本调试
启用此选项还需要启用开发构建,并且它会在脚本代码中添加调试符号。这将允许IDE(集成开发编辑器,例如 Visual Studio)在游戏运行时附加到游戏上,通过你的断点和调试系统进行调试。当你选择此选项时,将弹出另一个选项,如下图中所示:

图 11.2:等待托管调试器构建设置选项
如果启用等待托管调试器选项,它将等待 IDE 寻找构建并请求连接。在没有与调试器建立连接之前,不会执行任何脚本。
仅构建脚本
有时候你可能会发现一些错误并需要做出一些更改,但你不想构建一切。数据文件可能会变得非常大。我们在这本书中之前已经讨论过迭代的重要性几乎超过任何事情。这可以显著减少调试迭代之间的时间。
此选项将只构建脚本并保持所有数据文件完整。
压缩方法
这里有三个选项:默认、LZ4和LZ4HC。默认表示不进行压缩。不进行压缩的文件在 Windows、Mac 和 Linux 上可以直接运行。在 Android 构建中,它将构建为 ZIP 文件。
LZ4对于开发构建很有用,因为存储的数据将在运行时进行压缩和解压缩。场景和资产加载依赖于磁盘读取速度。这是一个可以用来帮助提高迭代速度的选项,因为构建时间比默认设置快。一个有趣的注意点是,Android 上的 LZ4 解压缩比默认的 ZIP 快。
LZ4HC是 LZ4 的高压缩版本,由于进一步压缩构建,构建时间会更长。这是一个在花费时间调试后用于发布构建的绝佳选项。
在游戏测试中从默认开始进行快速测试是个好主意。当你需要开发构建和调试时,使用 LZ4。然后当你准备好发布时,使用 LZ4HC 进行构建。
测试
游戏测试是一个广泛的概念。有一些更常见的测试部分和一些更小、更具体的部分。我们常见的测试模式包括:
-
功能测试
-
性能测试
-
游戏测试
-
浸泡测试
-
本地化
如果你研究游戏质量保证或游戏测试,你会找到几个其他测试的名称,一个工作室可能有他们自己的特定测试,这是他们的最佳实践形式。
以上内容均无错误。我们将从上面的列表中解释的测试在几乎每个工作室都能看到。让我们先分解列表中的第一个,功能测试。
功能测试
你的测试在你到达这一章之前就已经开始了。每次你按下播放来检查脚本是否按预期工作,你都在与游戏的其他部分一起测试那个脚本。这是游戏开发迭代性质的一部分,也被称为功能测试。
功能测试有一个非常直接的名字!它是测试游戏的功能。正在测试的功能范围的一些例子包括:
-
动画 – 寻找不兼容的动画或损坏的角色绑定。这是通过测试角色动画过渡到其他动画的机械和动作来完成的。
-
音频元素和质量 – 仔细聆听以在特定时间听到不完美之处。这类例子可能包括聆听听起来不正确的脚步声、不存在物体的环境噪音,以及声音放置不正确的地方。
-
电影场景 – 播放电影场景以查找任何不合适的声音、视觉效果、动画或整个电影场景的时序。
-
指令或教程 – 可能会有关于如何玩游戏的说明。这些说明应该写得恰当,并且对玩家使用的控制器方案有意义。
-
机械交互 – 通过体验所有机械操作来感受它们,检查它们是否按预期工作并且可以完成,如果存在完成条件的话。
-
分类 – 这是一个视觉检查,涉及透明度问题。屏幕上的图层需要知道它们在屏幕上的层级。一些效果和用户界面可能很难知道哪些在上面以便正确排序。这需要通过多种场景的测试来确保具有透明度的游戏对象能够正确排序。
-
用户体验 – 用户体验有其独立的工作线程,可以进行测试,但在这个案例中,我们寻找的是合理且有效的控制器方案。例如,A 按钮用于跳跃;这是如此常见,以至于如果使用其他按钮,就需要彻底解释为什么这样做。
-
用户界面(菜单结构、分辨率、纵横比、字体大小) – 用户界面需要彻底检查许多部分。缩放后看起来如何?颜色是否正确?能否理解菜单的流程?任何出现的小问题都会被大多数用户看到。这些问题需要被记录下来以便修复。
正如你所见,功能测试是彻底的,并且需要迭代以确保游戏的所有功能对玩家都有意义并且工作正常。当你玩游戏来测试单个机制时,这是一个很好的实践,但它只是在孤岛中检查那个机制。游戏的其他部分可能会受到你所做更改的影响。早期和经常进行强有力的功能测试将使你的项目最终更加干净和积极。
在进行功能测试时,您可能会遇到渲染中断,导致帧率降低。如果发生这种情况,请注意并将其添加到将要进行的性能测试列表中。既然我们提到了这个话题,让我们来看看如何在 Unity 中进行性能测试。
性能测试
在 Unity 内部,我们有四个分析来源:
-
Unity 性能分析器
-
内存性能分析器
-
帧调试器
-
物理调试器和性能分析器
这四个分析工具都有它们自己的特定用途,以帮助确定可能引起问题的原因。我们将简要介绍它们,以便熟悉每个工具。首先,我们需要了解最常用的一个,即 Unity 性能分析器。
Unity 性能分析器
要进行性能分析,您需要使用 Unity 的分析工具,即 Profiler,其外观如下所示图 11.3:

图 11.3:Unity 性能分析器窗口示例
性能分析工具有助于识别 CPU 使用情况、内存使用情况和渲染时间。当您通过窗口>分析>性能分析器打开性能分析器时,您将看到四个部分,如上图中图 11.3所示。这四个部分是:
-
(红色)性能分析模块 – 此部分包含正在使用的性能分析模块,并使用颜色显示性能分析开始记录时的活动情况。
-
(橙色)性能分析控制 – 这些控制用于设置性能分析器正在执行的操作。在这里,您可以开始记录并更改模式或性能分析工具。
-
(黄色)帧图表 – 这显示了随时间堆叠的图表和渲染过程曲线的各个帧。
-
(绿色)模块详细信息面板 – 模块详细信息面板解释了所选帧的每个部分,并按请求的线程的使用百分比分解。默认是主线程。
例如,在下面的图 11.4中,我们在玩游戏时选择了帧;我在移动 Myvari 时记录了 7800 帧。您可以看到我们当前每秒运行接近 60 帧。我想知道PlayerLoop中占用最多 CPU 时间的是什么——在这种情况下,是编辑器中运行的游戏,大约占 80%。向下滚动PlayerLoop,我们看到前向渲染器是主线程上最重的任务。场景中目前没有太多活动,这就是为什么我们平均每秒运行 60 帧。

图 11.4:已选择帧的性能分析器
您可以看到性能分析器附带了多少信息。当您想查看可能造成游戏帧率较低的原因时,这些信息非常宝贵。
内存性能分析器
如预期,这个工具分析了编辑器的内存。您还可以在构建设置菜单中选择开发构建时在独立构建上运行内存分析器。这里的限制是您不能在发布构建上运行内存分析器,就像我们之前看到的 Unity 分析器一样。内存分析器也不是自动添加到 Unity 项目中的。它是一个可以添加的包。要添加它,请按照以下步骤操作。
要添加内存分析器,请转到您的包管理器并按名称添加一个包。这可以通过选择如图 11.5 所示的选项,然后将com.unity.memoryProfiler添加到按名称添加包字段中完成:

图 11.5:(左)按名称添加包,(右)添加内存分析器
安装后,您可以通过转到Windows > 分析 > 内存分析器来访问内存分析器。当它首次加载时,您将看到一个空白的中部区域,并有一个创建快照的选项。如果您按下播放并然后拍摄快照,您将得到与我们这里相似的屏幕。在这个分析器中有很多信息需要查看。这个的主要用途是如果您有低帧率并且想要检查内存使用,或者在长时间测试期间看到崩溃。您可以通过多个时间点的快照看到正在使用的内存。

图 11.6:内存分析器
在查看快照时,中心区域会分解该快照中的所有内存。在底部区域,有用于组内的一切的块。你可以选择组来进一步分解它们。我们这样做是为了底部的紫色块,它用于显示Texture2D。我们已经知道这会有一个很大的内存占用,因为它是所有架构的纹理。它需要相当大。
如果内存看起来符合您的预期,那么查看帧调试器以检查可能引起问题的特定帧中加载了什么可能是个好主意。让我们接下来看看这个工具。
帧调试器
能够看到单个帧以及如何构建绘制调用以提供该帧的渲染,这对于调试视觉伪影非常有帮助。您可能在使用功能测试时使用过它,并发现了排序问题。现在您可以加载帧调试器并查看每个项目何时被渲染。从这个点开始,您将拥有知识来了解为什么它以错误的顺序被渲染。

图 11.7:帧调试器
帧调试器的另一个强大功能是您可以看到渲染项上设置了哪些 Shader 属性,如上图中图 11.7所示。这很有用,因为当您以程序方式设置 Shader 属性时,您可能期望 Shader 属性使用某些纹理或变量。如果它们不符合预期,那么检查帧并查看其设置将是一个很好的主意。这可能导致发现对于一帧,它被设置为预期的值,但随后被覆盖。这可能会帮助您找到在预期值设置后一帧或两帧更改您的 Shader 属性的脚本。
物理调试器和分析模块
对于物理,我们既有调试器也有分析模块。物理调试器是一个可视化工具,用于了解场景中有什么物理碰撞以及它们应该或不应该能够与之碰撞。如您在第七章,刚体和物理交互中看到的,刚体在物理属性的优化设置中很复杂。能够可视化哪种类型的碰撞器在哪里,以及它们在哪里,对于了解物体何时以及为什么这样做非常有帮助。在下图中,您可以看到场景中哪些对象是物理对象。
打开物理调试器的颜色部分,您可以根据需要调试的内容对它们进行着色:

图 11.8:物理调试器
在您通过物理调试器识别出由 GameObject 可视化的任何物理问题时,我们还有另一个工具来收集更多信息。幸运的是,我们还有物理分析模块,可以帮助我们处理任何正在可视化的物理问题,以便我们可以查找问题。物理分析模块位于 Unity 分析器中,将帮助您找到当调试器开启时可能看到的物理差异的答案。
要查看物理分析模块的外观,请参见下图的图 11.9:

图 11.9:物理分析模块
在这一点上,您可能不会理解这一点,因为我们没有与我们的物理力学相关的案例来展示直接的问题。我们没有快速移动的项目,这可能导致许多物理问题。如果您的游戏确实有快速移动的对象,当您记录分析器并注意到 GameObject 在它们不应该时穿过其他 GameObject 时,这个模块会很好,可以看到使用的总内存。可能使用的内存不允许物理更新得足够快,以获取活动体信息。
物理调试可能需要时间才能得到答案,因为这需要在物理工作正在进行时发生。对此调试要有耐心,并尽可能使用更多工具来获取您需要的答案。
现在我们已经讨论了所有的调试工具,我们需要用我们团队之外的人来测试游戏。这被称为游戏测试。让我们把游戏交给其他人来玩。
游戏测试
这是一个难以评估的问题。起初,对你和你的团队、朋友和盟友进行内部测试可能是个好主意。你让他们玩游戏,看看他们对它的感受如何。这并不是为了建立你的自尊。当他们玩游戏时,你需要在场,并要求他们大声说出他们对这次体验的感受。不要对他们进行任何提示。你希望他们给你真实的感受,如果他们针对预期的体验进行交流,你就走上了正确的道路。
即使游戏的艺术设计尚未到位,菜单系统是带有 Arial 字体占位符的方块物体,这些都丝毫没有减损核心游戏体验。一个例子是,当有人进入你想要激发的第一个情感触点时,产生的直观反应。对于我们项目来说,我们想要给人一种惊奇感。这接近于困惑,因此我们需要在放置和光线使用上谨慎,以鼓励玩家按照自己的意愿冒险。如果他们说诸如“我想知道那里有什么”之类的话,我们就是在追求那种惊奇感。
同时,他们可能会说出同样的句子,但你并没有计划任何内容。让他们探索他们认为可能存在的地方,并做好笔记。在那里放置一些东西是个好主意。让那种固有的好奇心将你的设计推进到你已经达到的范围之外。如果存在某种互动,那么那里有什么几乎无关紧要。一个例子是,一个与叙事无关的区域稍微偏离一侧,可供进入。当测试者进入你预料之外的地图上的位置时,把这视为一个互动的机会。你可以把它建成一个风景点。当你走过去时,摄像头会稍微向外移动,以摄入景色。这不是故事的补充,而是实现了对好奇心的满足。当你探索时,发生了某些事情。
探索可能不是你游戏的大部分内容;也许你的标题是一个以社交为中心的体验。在多人游戏中,任何玩家都需要高度互动。当你们一起玩游戏时,你们如何与朋友互动?一个极好的例子是 FromSoftware 的多人互动系统。FromSoftware 的游戏中的社交互动——例如艾尔登法环——允许你留下带有特定词语的简短信息,但你也可以使用表情符号,信息会播放你的角色作为幽灵,带着你的简短信息和表情符号。这允许你与可能找到的表情符号进行互动。这是在定义其让你感到孤独和脆弱的能力的游戏中允许互动的一种非常有趣的方式。
在他们玩完游戏后,记下所有笔记,并感谢他们花时间。你不需要实现他们测试中写下的一切,但当你有五个人测试它时,会出现趋势。首先关注这些趋势。
浸泡测试
浸泡测试不是一个直观的名字。我们不会把我们的电脑浸入浴缸;我们只是让游戏闲置运行 24 小时。你的角色在游戏中,是活着的,只是独自坐着,游戏在进行。这样做的原因是为了尝试找出游戏中可能存在的任何内存泄漏。
内存泄漏是指某些没有被妥善处理的内存。比如说,我们有一个粒子从树上落下,营造一些美好的氛围。粒子被设置为死亡,但是不小心多加了几个额外的 0,所以现在它不是持续 10 秒,而是持续 1000 秒。当你游戏中四处跑动时,这可能不是问题,因为当你离开时,粒子会被清除。但是如果你让游戏闲置,所有制造树叶的粒子系统都会堆积起来,地面上可能堆积数千片树叶,这可能会导致性能大幅下降。这需要修复,而且没有压力测试是无法实现的。
本地化测试
本地化是将游戏翻译成另一种语言以供体验的行为。这个测试可能比预期的要长,你需要有耐心。每个菜单项、对话行和描述都需要考虑这个测试部分。翻译也不只是逐字逐句的解释。一些语言需要更多的上下文来描述,如果不仔细注意,可能会导致非常混乱的翻译。
在本地化游戏时,要注意不要仓促行事。这可能会完全破坏另一种文化的体验,这将是件遗憾的事!
用户体验,或 UX
UX 可以被定义为品牌、设计和可用性中各个部分的和。就我们而言,我们将简要介绍品牌在 UX 角色中的作用。然后我们将只简单谈谈设计,因为我们已经覆盖了该项目基本部分的设计。在快速覆盖这些内容之后,我们就可以进入可用性部分。让我们开始吧!
品牌
为了广泛地涵盖这一点,通过 UX 视角的品牌化是关于用户在整个游戏旅程中体验的整体体验也需要在品牌中反映出来。用一个过于对比的例子来说,想想如果一个恐怖游戏的品牌使用了柔和的色调和粉彩,以及花朵和欢快的音乐作为他们的营销材料。这显然不符合游戏的品牌,会在用户的体验中造成不和谐。
UX 作为开发中定义的一部分的目的和要点是,要关注那些有意识的、连贯的行动。在 UX 上花费的时间应该确保所有设计部分,包括标志、营销材料和游戏部分,都是整体统一体验的一部分。
设计
到目前为止,这本书已经涵盖了大量的设计。有趣的是,我们以一种相对封闭的方式涵盖了所有的设计。有时这可能会引起问题,但幸运的是,对于我们的项目,我们真正专注于角色设计。其余的设计都是围绕她种族的过去构建的,这为视觉提示提供了答案。
游戏的节奏是通过游戏风格和机制专注于环境叙事来处理的。结合这三个部分,它已经是一个统一的项目了。这很好地验证了我们在每个部分上所花费的时间。做得好,坚持下来!
可用性
在通过您精美的品牌和智能而统一的设计吸引用户之后,他们应该能够使用该产品。对于游戏来说,可用性全部集中在交互上。这不应该让人感到惊讶,因为我们已经在第六章,交互和机制中将其定义为体验的支柱。我们已经与玩家进行了整体交互;然而,没有定义便利设施。我们需要弄清楚玩家如何知道他们可以执行这些交互。
我们将在这里概述垂直切片的主要部分,从到达第一个谜题的初始问题开始,然后转到第一个谜题本身。之后,我们需要介绍下一个机制——心灵感应,最后是最后一个谜题。
初始问题
在起始洞穴部分,我们有一个被封锁的楼梯,玩家需要与两个物体交互来解锁。我们将使用一些东西来给玩家提供便利,让他们知道如何执行所需的任务:
-
光池
-
世界空间 UI
-
满足动作
光池是环境或关卡设计的一个小部分,它让玩家感觉他们应该朝那个方向前进。如果一个隧道很暗,而隧道尽头有光,玩家往往会朝那个光走去。我们可以使用这种方法,在需要按下的按钮附近放置发光的物体或灯光。
现在他们已经接近了,当鼠标靠近足够近时,我们将弹出世界空间 UI 以进行交互。这个按钮应该与其他此类交互相同。对于这款游戏,我们的交互是键盘上的E键。
在使用交互按钮之后,需要有一些东西来满足该动作的使用。在这种情况下,它是一个岩石中的按钮。它将动画化以放置到位,发光以表示使用,并触发一个声音来完全指示用户已经使用了他们的便利设施。
第一个谜题
当你第一次来到第一个谜题时,可能不会立即理解玩家的目的是将石头移动到某个位置。我们使用了一个主要的关键可用性功能,以及其他一些看起来相似的功能。让我们再次列出它们:
-
光池
-
英雄艺术品
-
世界空间 UI
-
摄像机定位(之前提到的关键因素)
-
满足动作
我们之前已经讨论了光池化作为定位的概念。在这种情况下,我们将使用光池化来吸引视觉注意力到一个移动的地方。要注意的是门,因为它在你面前展示了谜题的答案。
我们光池化的艺术品是谜题的答案。它直接放置在你即将前往的下一个位置,当你从楼梯出来时,它就在你面前,并且是亮的。当你到达楼梯顶部时,没有障碍阻止你看到你应该看的地方。这就是我们为玩家所做的事情。让他们感受到探索的乐趣,但当他们这样做时,知道他们正在看对的地方。
我们的世界空间用户界面与之前相同,但我们使用它来让玩家知道当他们靠近可移动部件时可以与之交互。
在下一节移动的关键因素是摄像机移动。当你进入谜题的空间时,摄像机将缓缓上升到一个位置,这将代表当你第一次进入时,与门上的艺术品协调的柱子成功移动的位置。
当玩家将柱子移动到正确位置后,会听到岩石被放置到位的巨大声响,以及可能听起来像大锁芯落下的连接声。当最后一个柱子到位时,整个部件会移动到最终完成的位置,摄像机也会移回玩家的肩膀位置,同时中间的柱子升起,以便玩家按下“开门”按钮。
二级机制介绍
我们一直在解释玩家的可用性和如何引导他们执行动作。只要你小心地以故意的方式介绍它们,你就可以在不破坏体验的情况下添加新的机制。到目前为止,Myvari 只是一名探险者。我们希望她能从她古老的血液中获得轻微的心灵感应能力。我们只需让它们活跃并给她设置障碍,但这不是很有趣,体验也不够强烈。
为了给我们自己一个新的机制,并希望吸引玩家关注我们的主要角色,我们将采取两个最初不直接涉及玩家的动作,但它们确实涉及到 Myvari。我们首先会做的是,当 Myvari 在山的一侧狭窄地带行走时,一块大石头将从山上滚落。将发生一个电影般的场景,她会感到惊慌,并抬起手臂自卫,这会稍微触发她的心灵感应能力,她将稍微调整石头,使其从山上滚落,而不是落在 Myvari 自己身上。
接下来,在经过一些小范围的探索后,玩家会遇到一个他们无法进入的区域,但可以看到它。这里有一个柱子,看起来像第一个谜题中打开第一扇门的那个柱子,但它被分开了。我们将使用一种新的世界空间用户界面,当你悬停在破碎的部分上时,它会被勾勒出来,交互按钮会弹出。这个轮廓将与在心灵感应发生时用于岩石的颜色相似,当你与之交互时,你会伸出你的手并拿起它。当它靠近柱子足够近时,它会自动修复,这会触发你所在区域许多视觉上的变化。
总之,我们通过使用心灵感应的小例子来慢慢介绍,这种方式符合角色的特点。这是一个很好的可用性应用,因为玩家可以随着角色一起成长。现在我们可以将在这里学到的知识应用到最终的谜题中,该谜题将心灵感应作为主要的机制。
最终谜题
制作一个强大的体验需要付出多少努力真是令人惊讶!那些描述机制的前期努力是使体验成为沉浸式体验而不是仅仅按下一个按钮让角色做某事的粘合剂。
现在,我们进入了最后的谜题,这里有一些与之前看到的玩家操作手段相似的方法。我们将使用稍微不同的方法,因为用于机制中的物品不同,但整体概念是由环境驱动的。你将在最终的谜题区域看到这些用户体验功能:
-
光池
-
与英雄艺术品的连接
-
世界空间用户界面
-
满足动作
和往常一样,我们使用光照来帮助玩家微妙地理解下一步行动。有一个大光从我们的英雄艺术品(主要焦点)树后面发出,这意味着它有叙事含义,需要更多的作者处理,这显示了从连接到建筑的每根电缆中发出的光芒。如果你跟随电缆,它们会流经大柱子。
那些柱子与英雄艺术品(中心树)相连。问题是柱子本身缺少一些东西。那些东西就在树周围的多个位置的地面上。将鼠标移到这些较大的球形物体上,你会看到你可以与之交互。
在这种情况下,交互是世界空间用户界面。这是我们之前在拿起部件完成柱子时跨越水桥时看到的轮廓。拿起物体并将其移到相同形状的空位附近,会点亮电缆,需要按照一定的顺序完成,以便给树供电。这只会因为角色能够注意到一些电缆损坏或连接不正确而减慢速度。
满足动作来自于每次放置物体时从电缆流向树木的流畅能量。最终,树木将亮起,以电影般的方式打开一个区域,揭示一顶王冠。Myvari 抓住王冠,戴在她的头上,解锁了她作为她古老种族最后一位公主的位置。
当门户在完成拼图后开启,她兴奋地穿过它,这标志着垂直切片的结束。
摘要
你可能会想,“接下来是什么?”这是一个很好的问题。你有一个已经制作完成并经过一些测试的游戏,其中包含了一些错误修复。在这种情况下,它是一个可玩的项目,并且可以为投资者提供足够的背景信息,以继续获得游戏资助或发布的旅程。
我们接下来要讨论的是一些抛光技巧,旨在追求美观和用户体验。我们把这些最后的修饰称为“完成 touches”,因为我们知道垂直切片已经处于一个很好的状态,可以添加最后的修饰。在下一章中,我们将花时间查看所有可以推动我们的品牌和质量进入游戏的任务。
第十二章:完美细节
欢迎来到完美细节章节!关于游戏制作所需时间和游戏开发的总体难度有一个误解。本章将作为一个工具箱,指导你完成你的项目。这不是一个直接的下一步,而是一个开放的盒子,让你看到我们用来打磨我们的垂直切片的东西。打磨过程的有趣之处在于,它涵盖了游戏开发的 80%左右。这听起来可能不太直观;然而,如果你在开发过程中一直关注截图,你将注意到,从消费者的角度来看,我们在这个阶段根本不可能有一个完整的游戏。机制是有效的,游戏现在是一个体验,只是还不完整。
本章将涵盖以下内容:
-
概述
-
资产最终化
-
灯光
-
声音打磨
概述
完美细节对于完整的体验至关重要。我们需要把现有的东西都整理好,把所有的针脚都缝紧。这可以通过几种方式来完成。
在此之前,灯光和声音的最终确定非常困难。可能会有研究和开发(R&D),但除非游戏的重点是这两个主题之一,否则你不会在游戏中获得最终的灯光或声音,正如以下章节列表中所见。你正确地想知道为什么我们在之前有一章关于声音。我们想概述声音的基本知识,并让你熟悉声音设计和在 Unity 中的实现概念。
灯光可以提前工作以设定氛围,但需要在环境和灯光池定义良好并固定后才能最终确定。再次强调,如果灯光和氛围将体现在体验中,那么在开发阶段的初步阶段就需要进行大量的灯光研究和开发。这里关于灯光的所有对话也将在这个阶段为你提供指导,如果需要的话。
这将是这样工作的:我们将在本章的三个主要部分中涵盖特定的动作。这些动作特定于我们的项目,但同样可以帮助你未来的项目。把它们看作是工具而不是教程。其中一些可能需要编程,而另一些可能不需要。
由于大多数机制已经编程到一定程度,我们首先将关注资产最终化。记住,正如我们所说,如果资产没有完成,那么灯光和声音就很难得到。让我们从这里开始吧!
资产最终化
本节将非常精彩。这里有如此多的优秀艺术资产和完美细节可以探讨。以下是我们在未来项目中可能帮助到你的工具列表:
-
资产上的风格化通道
-
细节法线
-
建筑清理
-
纹理混合
-
环境杂乱
-
细节网格
-
特效
-
动画
-
二级动画
我们将如何浏览这些部分是,我们将解释为什么我们要为我们的项目做这件事,这可能会帮助你决定你是否需要在未来的项目中自己进行这些抛光处理。之后,我们将涵盖我们实际采取的步骤,以便你可以看到它们是如何完成的。有趣的是,我们实际采取的步骤可能不是实现这些最终修饰的唯一方法。采取这些行动的最佳方式是将它们作为一个概念或起点,因为你的项目需求可能会有所不同。我们将从对我们资产的风格化处理开始我们的最终修饰。
资产的风格化处理
在定义艺术风格时,我们首先从大笔触开始。即使你花时间概述艺术方向,一旦你进入抛光阶段,你将需要对其进行处理以放置最终修饰。在我们的案例中,我们发现我们的资产缺乏足够的风格化外观,以适应我们的艺术方向。风格化这个词经常被使用,并且它有权利经常被用于游戏,因为它意味着不仅仅是看起来不真实。在我们的案例中,我们希望风格化使一切感觉更具插图性质。这意味着我们需要将所有对比鲜明的轮廓和颜色推入纹理中。我们还需要在纹理中拥有更宽的线条粗细。
在我们的项目中,一个很好的例子是 Myvari 的项链。这件艺术品需要脱颖而出,因为它是 Myvari 心灵感应的主要焦点。我们也知道在电影中我们会近距离看到它,所以我们需要确保我们在这件作品上投入时间。

图 12.1:Myvari 项链的风格化处理
这需要在所有艺术作品中发生,以在角色和世界中尽可能保持一致性。一旦风格化处理完成,一些模型可能需要添加一些小细节。我们称它们为“细节法线”。现在让我们来了解一下它们!
细节法线
有时,细节法线可以被认为是风格化处理的一部分。在我们的例子中,我们希望这成为整体艺术指导中的一个突出部分,所以我们将其从风格化处理中提取出来。我们希望强调模型轮廓的风格化特性;然而,我们希望材料本身有一种现实感。皮革需要看起来像皮革,树皮应该看起来像树皮。在下方的图 12.2中,我们对蘑菇进行了细节法线处理,以给它增添一些额外的细微差别。左边的图像有基础法线和纹理。右边的图像在基础法线之上添加了细节法线。

图 12.2:左,无细节法线;右,添加了细节法线
细节纹理也很有趣,因为它们通常是来自可平铺纹理的较小细节,由于模型的纹理尺寸,它们不会很好地放在纹理本身上。为了获得小细节,我们在着色器中分层了它们。

图 12.3:细节法线
上图是我们用于图 12.3中细节法线的着色器。我们将通过跟随数据连接点并逐节点解释推理来分解它。首先,我们从 UV 节点开始。
UV 节点 – 此节点设置你将操作的 UV 空间。下拉菜单允许你选择要操作的 UV 图。由于我们使用主 UV 通道,我们将它保持在UV0。我们将 UV 节点的输出输入到一个 Swizzle 节点。
Swizzle 节点 – Swizzle 节点允许用户取一个输入并混合通道以创建所需数据量的输出。你会注意到我们已将输出设置为xy。我们的输入是一个引脚线,它指的是一个Vector4,这也在 Swizzle 的输入中显示。在这种情况下,我们只需要红色和绿色通道,所以我们只请求xy或rg通道,我们得到一个输出绿色线的Vector2。Unity 的Shader Graph已经裁剪了其余的通道,所以我们不需要特别这样做,但只使用你需要与之工作的通道是一个好习惯。我们将这个输出输入到一个乘法节点。
乘法节点 – 我们在这里使用一个浮点参数来提供 UV 的可定制性,以及 Swizzle 输入。Detail Normal Scale参数被公开,这样我们可以在检查器中稍后进行更改,调整以满足我们的需求。这个输出的结果将进入一个采样纹理 2D 节点的 UV 通道。
采样纹理 2D 节点 – 此节点的另一个输入是我们的纹理 2D 参数细节法线。我们需要确保空间选项设置为切线,因为我们将在后面影响切线以重建法线。我们将输出取出来,再次得到一个Vector2,但使用的方法不同于 Swizzle。我们将使用采样纹理 2D 节点上的单独通道的合并节点。
合并节点 – 从采样纹理 2D 节点的输出中获取 R 和 G,我们将它们合并以创建一个Vector2,它正在采样我们想要的纹理并遵循我们设置的 UVs。现在我们需要将这个Vector2输入到一个缩放节点并对其进行偏置以进入不同的范围。
缩放和偏置节点(使用乘法和减法) – 下两个节点是一个基本的数学函数,用于将(0 到 1)的范围转换为(-1 到 1)的范围。我们通过将 2 乘以并在 X 和 Y 向量上减去 1 来实现这一点。这对我们来说很重要,因为我们可能希望法线看起来是凹的,或者进入模型。在完成这个函数后,我们将输出输入到一个法线重建 Z 节点。
正常重建 Z 节点 – 这个节点的目的是从我们在样本纹理 2D 节点中选择的正常图中推导出 R 和 G 的正确 Z 值。
之后还有三个更多步骤。我们将遵循这些后续步骤的单独图。我们将把这个节点的输出移动到正常强度节点中。
正常强度节点 – 连接到正常强度节点的是我们从正常重建 Z 节点得到的法线。还有一个浮点值,我们为其创建了一个名为详细法线强度的参数。这可以在下面的图 12.4中看到。我们使用这个节点是为了如果法线图看起来可能细节过多或者视觉上不吸引人,我们可以稍微降低一些。我们在强度输入中设置的参数允许我们动态地为每种材料设置详细法线强度。

图 12.4:正常强度节点
我们将这个输出放入正常混合节点中。
正常混合节点 – 我们最终希望这些详细法线与网格本身的法线分层。这就是我们将要执行的节点。

图 12.5:正常混合节点
它将输出一个包含数据内部法线的正常图。然后我们将输出放入布尔关键字参数中,我们将其命名为“详细法线?”。
布尔关键字 – 这个布尔关键字设计成允许我们选择是否使用详细法线。由于这个着色器被用于许多材质中,我们需要一种方法来排除如果网格可能没有详细法线时不需要的情况。我们通过将“开启”的输入设置为网格的混合法线和详细法线来实现这一点。如果设置为“关闭”,则只接受网格法线。

图 12.6:详细法线布尔关键字
这个输出将随后进入主堆栈正常输入。当你创建一个材质时,如果你想有一个详细法线,你只需要通过“详细法线?”参数的复选框选择“开启”。
接下来,我们将清理架构。
建筑清理
当前建筑的轮廓可能看起来不错,但建筑本身是否合理?这是一个关于建筑形状的有趣设计问题。我们需要确保建筑看起来像是可能由一个活物建造的。这是一个很难做对的任务,因为我们试图模仿的生物并不存在!它们是虚构的生物,这意味着我们在为它们进行建筑规划时需要非常清楚我们所走的路径。
我们知道它们专注于天体和时间概念。空间、行星的形状和时间的概念需要参与到建筑的轮廓和材质中。这可能并不意味着对部件进行彻底的改造,而是更多地推动形状,使语言足够突出,以适应我们正在设计的文化。
我们还需要去除一些永远不会被看到的几何形状。这是为了优化游戏,并且非常重要。在游戏中,如果你看不到它,那么它就不需要渲染。因此,我们做了一些称为背面裁剪的事情。这意味着如果你从内部看球体的背面,它将是不可见的。
物体的背面不会被渲染,因为它看不见。如果你不这样做,球体将不得不渲染所有内部面,这将浪费宝贵的计算机时间;我们需要渲染其他所有东西。
纹理混合
当构建地形或需要连接的大物体时,总会有一些线条显示物体是 3D 网格。这是一个常见问题,如果不密切处理,可能会损害沉浸感或破坏体验。有几种方法可以使这个问题变得更好。你可以在分裂的上方添加另一个网格。你也可以层叠或重叠网格,在模型中制造一个断裂,让玩家认为它是故意稍微破损的。你也可以执行一种称为纹理混合的操作。
我们实现这一点的其中一种方式是通过 Y-up 材质。它们可能也有其他名称,但我之所以这样称呼它们,是因为它们使用 Y-up 轴来混合材质。我们所做的是要求着色器混合世界法线值的正 Y。我们在着色器中使用这个值,其中基础纹理位于 A 通道,B 是苔藓或雪纹理。让我们看一下下面的图 12.7至12.9,以查看 Shadergraph 图像的截图。在图 12.7中,我们展示了带有单个 UV 集和岩石纹理的一些岩石。这些岩石完全相同,除了我们复制了它们并将它们旋转以展示我们组合的着色器,该着色器将纹理放置在世界法线上。

图 12.7:应用了我们的 Y-up 着色器的岩石
应用到这些上的纹理不是最终的苔藓纹理,但它们被设计成与岩石形成对比,以分别显示纹理。这使得我们能够通过视觉轻松地处理这些差异。你会注意到岩石是相同的,但已缩放和旋转。这是在场景中的网格中提供重用的一种强有力的方式,这样你就不必建模那么多岩石!接下来让我们看看Shadergraph是如何实现这一点的。

图 12.8:Lerp 的 T 值的世界正常 Y-up
我们需要规划如何在网格上分割纹理的渲染。有趣的是,我们需要确保纹理无论如何旋转都能始终出现在网格的顶部。我们决定使用世界空间的法线向量,然后乘以一个我们命名为Offset的Vector3。我们想要正 Y,所以Offset参数的默认值将是(0, 1, 0)。我们还有两个额外的混合参数。它们是Blend和Level,它们都是浮点数。Blend参数是一个从 0 到 1 的硬值。当值为 0 时,没有混合,岩石是唯一的纹理;当值为 1 时,没有混合,其他纹理有一个硬的线条。这与Level参数相辅相成。Level参数应该设置为滑动条,最小值设置为 0,最大值设置为 100,默认值设置为 1;这些可以在图检视器的节点设置中设置。我们添加它在这个着色器中是为了展示你可以为你的艺术家添加更多工具。在这行数据的末尾是一个饱和度。
这确保了数据保持在 0-1 的范围内,这是我们需要的 Lerp 的 T 值,我们将在下一部分讨论。

图 12.9:纹理查找和 Lerp
如上图图 12.9所示,我们的 Lerp。值是基础纹理,B 是 Y-up 纹理。T 是图 12.8中饱和度的输出。Lerp 的输出将进入我们的基础颜色。这只是开始,你可以通过使用法线贴图和高度贴图来帮助混合通道,使它们更加无缝。我们目前在这个着色器中不使用额外的贴图,但这个概念使用的是完全相同的节点,只是增加了作为输入的贴图。
环境杂乱
这是一项完全独立的工作。那些与环境杂乱工作的人在该行业中被称为杂乱艺术家。他们的工作是放置物品,使环境看起来有人居住。目前,我们有一个机械设计的环境。我们知道 Myvari 需要在哪里触发电影。我们知道她将如何与物理谜题合作。但我们不知道人们以前是如何在这个空间中生活的。
在有谜题可以打开门之前,这些空间是用来做什么的?周围应该有破碎的东西,或者是不是很久以前就都破碎了?应该有蜘蛛网或者植物覆盖在某个部分上吗?
杂乱艺术家将有一套小物品来放置,以营造出这里曾经发生过一些事情的感觉。这就是我们有机会在每个部分讲述小故事的地方。
细节网格
Unity 地形可以容纳细节网格,以放置简单的网格,例如草地或小石头。我们在第五章,环境,绘制细节部分中简要解释了这一点。这个章节的主要原因是要解释还有更多的工作要做在细节上。这与杂乱艺术家的工作非常相似;然而,这并不是特定于空间是如何被居住的,而是为了发展自然。在我们的案例中,我们正在使用它来处理草地和石头。我们需要确保草地和石头位于一个有意义的地点。
这主要是通过清理场景的细节网格来工作的。
效果
抛光效果类似于抛光动画。它们需要经过精心调整以确保能够激发观众正确的情感。在这个垂直切片中的大多数效果都旨在营造氛围。我们将介绍两个效果。第一个将是洞穴第一部分楼梯的阻挡器。第二个将是 Myvari 的意念移物。我们选择这两个效果在书中进行介绍,因为它们彼此之间非常独特。
楼梯阻挡器
楼梯阻挡器是为了在玩家上楼梯时设置障碍。他们需要找到一种方法来禁用这个障碍,以便他们可以继续前进。我们决定使用在楼梯前方向上移动的神秘能量。这将完全通过着色器来完成,这意味着我们将介绍 Shader Graph 中的某些简单技术。
这里显示的图像,图 12.10中的效果是静态的,所以请进入项目并查看楼梯前面的第一个谜题区域。

图 12.10:楼梯阻挡效果
这个效果是通过利用一个包含三个独特云纹理的通道打包纹理来制作的。云纹理是 Adobe Photoshop 中的灰度 Perlin 噪声。我们将每个图层放置在红色、绿色和蓝色通道中,以便在一个图像中拥有三个纹理。这使我们能够在动画其 UVs 时使用多个不同的云来构建我们自己的噪声模式。为了使这个效果工作,我们需要一种方法来以多种方式动画这些 UVs。我们选择了一个 A 集和一个 B 集,我们在参数中创建了它们。让我们逐一介绍我们的所有参数,以确保我们处于同一页面上。随着我们从下面的图 12.11中看到的效果逐渐展开,我们将解释为什么我们有每个参数。
我们有颜色,这将设置神秘魔法的整体颜色。云纹理将是可用于此着色器的纹理。然后我们有偏移和平铺,两者都有 A 和 B 版本。我们很快就会介绍这两个参数。然后我们有两个用于 Smoothstep 节点的边缘。
我们首先需要弄清楚如何使我们的纹理动画化。我们将使用平铺、偏移和云纹理来执行这个着色器的初始部分。

图 12.11:来自 Blackboard 的 StairShield 参数
查看下方的图 12.12,我们之前已经看到了 Sample Texture 2D 和Multiply节点。现在让我们回顾一下Time节点。这个节点为您提供了游戏时间、游戏时间的正弦和余弦值、delta 时间和平滑的 delta 值。我们将使用游戏时间并将其乘以一个常数值来获得我们的速度。下一个新的节点是Tiling And Offset节点。这个节点是一个实用节点,用于帮助处理将材质应用到网格上的 UV 的平铺和偏移。我们将Vector2偏移量分配给时间的乘积。这将为我们提供移动的偏移值。这将使 UV 按照您希望它们移动的方向进行动画。
最后的部分是将 Tiling And Offset 节点连接到 Sample Texture 2D 节点的 UV 输入。您没有看到这张图中的偏移和 Tiling B 集,因为它们是具有不同参数的相同节点。我们想要多个集合的原因是我们想要有不同速度和 UV 平铺比例的独立纹理。这使得输出中的纹理具有动态效果。

图 12.12:云纹理的偏移和平铺
我们需要组合一个看似永无止境的平铺图案。所有这些噪声图案都在水平和垂直方向上平铺。有时这被称为四向平铺纹理。我们计划将偏移 A 在 Y 轴上以更快的速度移动,然后偏移 B 稍微慢一点。我们还会在 0.5 到 0.75 之间平铺 B 集。这将给我们提供一套完全不同的噪声,可以叠加在其他噪声之上。

图 12.13:通道交叉
在上方的图 12.13中,我们正在制作三个动态图像以组合在一起。两个 Sample Texture 2D 节点具有不同的平铺设置和随时间移动的不同偏移量。将它们通过乘法组合在一起,当它们交叉路径时不可避免地会形成一个活生生的云结构。我们用所有三个通道(R、G、B)来做这件事。接下来,我们将每个通道乘以 5,以强制整个图像通道高于它们的原始值。然后,我们通过将前两个乘法节点相加,然后再加上第三个节点,将三个通道相加到一个输出中,如下面的图 12.14所示。

图 12.14:乘法和加法
现在我们有一个带有运动的单个数据流,我们可以推送值来制作更有趣的效果。我们喜欢平滑步进数据,将接近 0 的任何东西推到 0,将接近 1 的任何东西推到 1。这使得分层数据在下面的图 12.15中呈现出有趣的外形。这个问题是,在这个过程中,整体云层感丢失了,因此我们想要添加之前的添加,然后饱和它以确保它在 0-1 的范围内,然后乘以一个颜色参数,以便在检查器中更改颜色。

图 12.15:平滑步进和颜色
颜色节点的输出将进入基础颜色。然后我们制作一个使用SH_StairShield着色器的材质,并将其应用到场景中的一个平面上,我们想要展示那里有东西阻挡楼梯。
飞镖系统 – 楼梯阻挡粒子层
我们喜欢楼梯阻挡的感觉,但效果需要层次感才能显得像精心制作的艺术作品。我们还花了一些时间研究飞镖本身。这个效果将覆盖飞镖的一些基本部分,以产生简单的效果并将其分层到您的世界中。我们将创建一个向上拉伸的精灵,以给楼梯阻挡者更多的能量。
首先,我们想要制作一个带有默认物品的东西来展示粒子系统的强大功能。我们使用的是ParticlesUnlit材质,它是一个从中心开始的简单径向渐变。我们有时称这些为“数学点”,因为它们可以不使用纹理创建。我们想要生成具有大量向上能量的粒子,但在生命周期的末尾减速并逐渐消失。我们将通过下面的设置来实现这一点;然而,我们鼓励您查看项目中的粒子系统并调整设置。尝试一些更改,看看您是否能制作出您觉得看起来更好的效果。在 Discord 上分享它!
飞镖系统在模块内部有大量的参数。我们只会讨论我们修改并需要启用以实现这个简单系统的参数。我们强烈建议您查阅 Unity 文档以了解所有参数和模块的解释。让我们首先看看主模块,如下面的图 12.16所示。

图 12.16:飞镖主模块
在这里我们更改的唯一参数是开始生命周期,将其更改为 1.4,以及开始速度,设置为 0。我们在做出所有其他更改后更改生命周期,因为我们不知道这个粒子系统需要存活多长时间。我们将开始速度设置为 0,因为我们知道我们想要控制速度。我们还修改了颜色,但稍后将在颜色随生命周期变化模块中覆盖颜色。接下来我们将讨论的是发射模块。

图 12.17:发射模块
如上图图 12.17所示,这是发射模块。我们将随时间变化率设置为 30,以确保有足够的粒子生成。你粒子的发射高度依赖于你需要传达的内容。对我们来说,我们希望有足够的粒子添加到楼梯屏障着色器中,但不要太多以至于压倒它。
现在我们有一群粒子正在生成,但我们知道我们希望它们在楼梯阻挡器的底部附近生成。我们将使用形状模块来限制生成到对效果目的有意义的地点。


图 12.18:形状模块和放置在游戏中的形状
我们选择形状为盒子,因为我们希望粒子从楼梯阻挡器的底部生成,并从那里向上移动以跟随运动的流动。然后我们需要让这些粒子移动。我们知道我们希望它们快速向上移动,因此在下面的图 12.19中设置线性 Z为 100。这把它们发射到太空中,但我们希望向我们的速度添加阻力成分以在顶部附近减慢它们的速度。这来自于限制生命周期内速度。

图 12.19:生命周期内速度模块
下面的图 12.20显示了我们将在哪里添加阻力到我们的粒子。我们将阻力保持在一个恒定值,并将其设置为 5。这个值给了它一个很好的阻力。这个值事先并不知道;我们只是随意调整,直到它感觉像是我们要找的东西。

图 12.20:生命周期内速度限制模块
接下来,我们需要给这些粒子上色,因为它们只是向上移动的白色数学点。启用下面的图 12.21中看到的生命周期内颜色模块,允许你定义一个渐变,其中左侧是粒子的开始生命周期,右侧是粒子的结束生命周期,包括粒子的 alpha 值,如果你的材质设置为接受 alpha 值。
![img/B17304_12_22.png]
图 12.21:生命周期内颜色
点击渐变将弹出渐变编辑器,如下面的图 12.22所示。渐变的顶部是 alpha 值,底部是颜色。尝试更改它们上的颜色,看看它如何改变粒子!

图 12.22:渐变编辑器
现在我们从 Renderer 模块设置渲染模式。因为我们从一开始就知道我们想要的粒子应该从速度拉伸出来,所以我们很早就将这个设置改为 Stretched Billboard。如果你决定跟随这个粒子创建过程,你的粒子将看起来像彩色点而不是条纹。
将 Render Mode 更改为 Stretched Billboard 将解决这个问题,如下面的 图 12.23 所示。我们还设置了 Speed Scale 为 0.1,因为它们移动得非常快,如果你将 0.1\ 以下设置得更高,它们会拉伸得更远。

图 12.23:Renderer 模块
通过这些示例,我们仅仅展示了一个拉伸粒子的简单例子,以展示一些可用的系统。当你给粒子添加着色器时,力量就显现出来了。精心设计的视觉效果可以触发动作发生的情感。虽然一开始这可能看起来有些令人畏惧,但如果你分解你需要的东西,它就变成了更多是享受与设置玩耍的时间,以获得满足你需求的感觉。当你进入项目时,你会在关卡周围看到其他 Shuriken 效果。请随意将它们拆分并重新组合,以了解设置的不同之处以及它们在视觉效果角色中的扮演。
我们将在下一节中介绍 VFX 图形。这是一个允许我们创建 GPU 粒子的另一个粒子系统创建器。这是一种不同的工作方式,因为它在检查器之外有自己的系统设计和 UI。让我们来看一下我们在项目中使用的示例。
VFX 图形 – Myvari 的心灵感应
心灵感应可以看起来像任何东西。我们希望它看起来像是 Myvari 正在利用从她那里流向她所控制物体的天体能量。对于这部分,我们将介绍我们如何设置整个 VFX 图形、着色器和一些实现代码。
我们假设你已经安装了 VFX 图形包,并且已经打开了 FX_BeamSetup 视觉效果资产。
Spawn 上下文默认情况下包含一个在此上下文中的恒定生成速率块。我们只想一次性爆发 32 个粒子,我们希望只要条带存在就操纵这些粒子。我们删除了恒定生成并放置了一个 Single Burst 块,如下面的 图 12.24 所示。

图 12.24:生成上下文
数字 32 最初并不是一个特殊的数字。我们不确定我们需要多少,但在创建条带的过程中添加更多是很简单的。下面的图 12.25是我们的初始化上下文。我们需要将每条带粒子数设置为与上面爆发的生成相同的数字。我们需要一个设置大小块和一个设置自定义属性块。这个属性块将是一个浮点数据类型,我们称之为InterpolatedPosition。
我们之所以这样称呼它,是因为我们想要每个粒子的索引,这样我们就可以将它们分别放置在我们想要的位置。

图 12.25:初始化上下文
我们可以在下面的图 12.26中看到,我们正在获取粒子索引,然后将其除以总数减一。索引从 0 开始,因此我们需要从生成的数字下面开始。这给我们一个可以工作的值,并将其存储在我们创建的浮点自定义属性中。

图 12.26:粒子索引节点
现在我们有一个需要定位的粒子条。我们将在黑板上创建两个变换参数,就像我们在 Shader Graph 中做的那样。我们给它们命名为BeamStart和BeamEnd。我们将根据我们初始化的插值位置浮点数将粒子的位置从光束起点插值到光束终点。查看下面的图 12.27,你可以看到我们是如何将它们连接在一起的。Lerp 的输出将进入更新上下文。

图 12.27:定位光束
在更新上下文中,我们有两个块,如下面的图 12.28所示:设置位置和添加位置。我们将把 Lerp 的输出位置添加到这个块中。有一个技巧会使一些奇怪的移动发生。在设置位置块中,中间有一个小的w。如果它是L,那么这意味着它在移动局部位置。这将在移动 GameObject 时导致双重变换。如果你点击L,它将变成w,代表世界空间。将添加位置留在局部空间是完全可以的。

图 12.28:更新上下文
目前我们有一个从起点到终点的直线光束。这对于测试来说很好,但我们需要一些更有趣的东西。让我们添加一些湍流,使其不那么僵硬。我们将使用添加位置块,其输入将是 3D 噪声的一些操作。这需要更多的节点来制作出漂亮湍流所需的数据,但我们将逐一介绍。
查看下面的图 12.29,我们需要的节点只有这五个。我们想要获取当前位置,然后将其添加到时间上。中间有一个乘法节点,因此我们可以加快或减慢时间值。这也可以是一个可调的变量。在加法之后是Perlin 噪声 3D。这里的值完全是主观的。将你的坐标放入坐标槽中,然后将输出导数放入更新上下文中的添加位置块输入。从那里,调整这些值,直到它给你想要的良好湍流。这种方法有一个问题。这将更新每个粒子,包括光束的起点和终点。这感觉有点奇怪,因为我们希望它从角色的手中发出。

图 12.29:用于湍流的 3D Perlin 噪声
为了确保光束的起点和终点与这个效果独立,我们采用了简单的渐变来告诉位置是否应该使用湍流。查看图 12.30,我们看到我们取插值的位置值,并使用时间进行插值采样。现在渐变充当了一个传递,决定了哪个粒子会受到影响的。条带开始和结束处的 0 值将使 0 值与噪声发生器的导数相乘。现在我们将这个值插入到添加位置块中。

图 12.30:湍流的遮罩
我们正在设置 VFX 图形部分的最后阶段。输出上下文在图 12.31中显示。默认情况下,这将是一个输出粒子四边形。这对我们没有任何好处,所以如果你在 VFX 图形中有它,请删除它,然后按空格键创建一个新的节点。然后输入particlestrip。你正在寻找的是输出粒子条带四边形。下面的是带有“unlit”名称的,这是由于使用的材质造成的。

图 12.31:输出粒子条带四边形上下文
该着色器是SH_StairShield的一个副本,但有一个改动。在图形检查器中,支持 VFX 图形布尔值设置为 true。这个着色器现在足够灵活,可以完成这项工作。我们可能在最终使用之前更改纹理,但就目前而言,它已经包含了我们启动所需的一切。然后我们将它分配到输出上下文中的 Shadergraph 属性。这将暴露着色器中的参数。
要最终完成这个效果,还有两个步骤。我们需要创建 GameObject 的光束起点和终点,然后在游戏过程中放置 GameObject 的位置来实现这个效果。
首先,让我们制作我们的预制件。在下方的图 12.32中,我们创建了一个空的 GameObject,并将其命名为Telekinesis。然后我们将光束设置对象作为子对象放置,并将其位置设置为0, 0, 0。然后我们创建了另外两个空的 GameObject,并将它们命名为BeamStart和BeamEnd。我们也将这些位置设置为0, 0, 0。

图 12.32:心灵感应预制件
你可以向 VFX Graph 资产添加一个名为VFX Property Binder的组件。将此组件添加到FX_BeamSetup GameObject。然后我们创建两个绑定属性并将其与变换绑定,并命名为 VFX Graph 中的属性(BeamStart和BeamEnd)。将 GameObject 拖入目标槽位以引用 GameObject 的变换。对BeamEnd也做同样的操作。这将在下方的图 12.33中看起来。

图 12.33:VFX Property Binder 组件
现在我们需要了解实现方法。这里的考虑是,光束的起点需要来自我们的角色的左手。我们还知道我们需要将终点连接到我们用物理控制的物品上。我们还需要在交互按钮与物理谜题物品交互时才打开和关闭视觉效果。我们将使用DragRigidBody.cs。
此脚本以屏幕中心作为参考点,如果你在可以与之交互的物理物品的范围内,它将通过我们在第六章,交互和力学中讨论的物理谜题部件脚本,将 Myvari 控制该 Rigidbody。
需要添加的字段:
public VisualEffect telekinesis;
public Transform leftWristLoc;
public Transform beamStart;
public Transform beamEnd;
这些将在编辑器中分配,应该一目了然,除非可能是leftWristLoc。这个变换来自 Myvari 的关节层次结构。展开她的层次结构,并将左腕拖到检查器中的这个槽位。
在更新中,我们希望在交互按钮释放时关闭光束。
if (control.wasReleasedThisFrame)
{
//Release selected Rigidbody if there any
selectedRigidbody = null;
telekinesis.enabled = false;
}
在此之后,我们需要处理FixedUpdate。我们正在处理物理,因此我们需要程序检查我们是否有 Rigidbody,在FixedUpdate中,如果为真,我们将打开光束,并在每个FixedUpdate循环中用物理设置beamStart和beamEnd的位置。
if (selectedRigidbody)
{
telekinesis.enabled = true;
…
beamStart.position = leftWristLoc.position;
beamEnd.position = selectedRigidbody.gameObject.transform.position;
}
就是这么简单!保存你的文件,回到编辑器,并将变换和视觉效果分配给脚本。此脚本位于Main Camera。下方的图 12.34显示了带有脚本的选定对象。

图 12.34:用于心灵感应脚本的 Main Camera 位置
粒子效果和着色器工作总是需要小心处理的有意思的问题。好事做过了头反而不好。在处理关卡时,花点时间思考一下细节,看看是否需要添加小动作来增强体验。
从上述两种效果来看,每个视觉效果都投入了大量的思考,无论效果的大小。花时间逐一分析游戏中的每个效果,分解其组成部分。
电影片段
在我们的项目中,我们使用电影片段有三个目的。第一个目的是解释这个区域已经存在很长时间了,所以这些区域很脆弱。第二个目的是向玩家展示 Myvari 拥有天生的力量,因为她通过自卫来对抗滚落的巨石。第三个电影片段是当她戴上王冠并通过传送门完成最终谜题后的结尾场景。
我们处理电影片段的方式是在它们在环境中就位时导出模型。这使我们能够确保我们的电影片段尽可能精确地与环境匹配。
二级动画
有时候需要额外的动画,这些动画比绑定和手动关键帧更容易模拟。头发就是一个很好的例子。头发所采取的动作是在获得动量之后的二级动画。手动关键帧是可能的,但需要大量的耐心,而使用物理方法则可以完成。我们将使用 Unity 的弹簧关节组件来完成这项工作。Unity 的 Asset Store 中也有一些资产被制作出来,以使这个过程更加稳健。如果你只需要简单的物理来处理二级动画,可以通过 Unity 的物理 Rigidbody 组件、弹簧关节组件和胶囊碰撞器来完成。
照明
我们已经决定在最终阶段添加照明效果,但这可能需要一本自己的书来详细阐述。这是那些可能变成巨大兔子洞的话题之一。我们在这里想概述一些照明的基础知识,以及为什么重视照明的重要性,同时还会介绍一些抛光工具以及如何在 Unity 中使用照明。
首先,我们需要解释照明是一门艺术。照明的目的包括定义 3D 模型、提供氛围以及设计游戏玩法的方法。在讨论了几个关于照明的构思之后,我们将游览 Unity 的混合照明、光照贴图、反射和光照探针。
3D 模型
没有照明,3D 模型看起来是平面的。实际上,我们大多数效果都使用未照明的着色器。一个原因是,我们不需要为那些只会在屏幕上短暂显示的小型闪亮效果添加阴影和照明。它们是平面的,不需要照明来定义它们;它们的纹理形状已经完成了这项工作。
营造氛围
这与区域设计相一致,但专注于光照。当你走过小巷时,小巷是否变得越来越暗?这可能会给你的玩家带来危险或紧张感。你希望某些区域周围有不自然的照明颜色,以在法师的家中营造出一种神秘感吗?完全可以实现!在放置光照时,应该考虑所有这些决定。与氛围一样,我们可能希望我们的灯光定义游戏玩法。
游戏玩法设计
游戏玩法可以通过多种方式通过光照来定义。实际上,你的整个游戏可以围绕光照来设计。
恐怖游戏经常使用光源作为推开敌人的方式,但它的限制是一个小计时器,因为你的电池不可避免地会耗尽!采取一条独特的路线,一款名为 Boktai 的老游戏使用 Game Boy 的光传感器外设来充电你的武器,如果你在黑暗中玩这款游戏,游戏会更难。
这些概念在游戏玩法元素中有点边缘。我们只需使用光照让玩家知道去哪里,或者避开哪里。我们现在可能已经对光照设计的通用概念以及它如何影响玩家的体验有了很好的了解。让我们深入了解 Unity 照明。
Unity 照明
要达到一个光洁的状态,我们首先需要了解基础知识。这将是一个概述,说明你在 Unity 中可以进行哪些照明操作,然后我们将讨论我们项目中的设置和用途。内置渲染器、URP 和 HDRP 照明彼此不同。我们将具体讨论 URP 照明。我们还将推动一种特定的感觉,并解释帮助我们实现垂直切片中期望的外观的功能。每个照明资产都可以以不同的方式配置,这意味着这些步骤只会提供所需帮助,以使你对照明有所了解。在你了解这些并尝试我们所解释的内容之后,我们强烈建议阅读其他照明对象的文档,根据项目需求,针对不同的渲染管线。现在我们已经讨论了光照的结构,我们将从混合照明开始讨论。
混合照明
我们在这里采取了一种小捷径,直接从混合照明开始。要正确使用混合照明,你需要使用间接烘焙光照和动态光照。我们现在将讨论这两者,然后回到混合照明。
间接烘焙光照
实时灯光,将光线投射到静态 GameObject 上,这些 GameObject 在世界的几何形状上反弹,将被烘焙到光照贴图上。这些术语是新的!静态游戏对象通过在检查器中选择静态复选框来定义,如下面的图 12.35所示。

图 12.35:静态复选框
当选中此选项时,当游戏将光图烘焙到光图 UV 中时,它将知道要将此添加到烘焙的物品中。只有当您确信您永远不会移动您将设置为静态的 GameObject 时,您才会选择此选项。我们相当确信这个混凝土栅栏在整个游戏中都将保持稳固,所以我们将其选为静态。下一个术语是光图。这是一组不允许与您想要烘焙光照的对象重叠的 UV。当您导入模型时,可以让 Unity 为您生成光图 UV,它在这方面做得相当不错。您可以通过选择 3D 模型的 FBX 并选择生成光图 UV来完成此操作,如下面的图 12.36所示。
当您选中复选框时,将显示光图 UV 设置。这些值是您场景中每个对象的平均值。这些设置将很好地设置基础,但您可能需要查看每个属性以确保每个对象以您期望的方式接收光照。

图 12.36:生成光图 UV 选项
这是对接收光照的物体而言。至于灯光,您可以将任何可用的灯光设置为烘焙光。方向光、聚光灯、点光源和区域光都可以在生成或烘焙光照时添加到光图中。
动态照明
这也被称为实时照明。实时照明必须处理实时阴影以及与此相关的许多设置。实时照明应用于未被选为静态的任何物品。骨骼网格始终是实时的,因为它们不能是静态的。它们的本质就是移动!
在我们的 URP 资产中,我们可以看到在阴影设置中,我们可以设置阴影质量下降的距离。在下方的图 12.37中,您可以在阴影部分看到这个范围。

图 12.37:URP 阴影设置
每个实时灯光都将使用这些设置来处理阴影。级联是指灯光质量下降的次数。默认情况下,它以米为单位设置。这可以帮助我们设计限制,因为我们知道我们的角色通常有多高。默认情况下,1 个 Unity 单位是 1 米。您可以设置一个测试场景,以查看每个级联距离的阴影效果,以帮助做出这些决定。
实时灯光的独特之处在于有四种可用的灯光。
方向光、点光源和聚光灯都可用于实时照明信息。区域光不能创建实时阴影。
现在我们已经了解了实时和间接照明的基础知识,我们需要回到混合照明模式。首先,我们需要告诉你如何在场景中放置灯光。在下面的图 12.38 中,你可以看到灯光列表。你可以像创建任何 GameObject 一样访问这个菜单,通过在层次结构中右键单击或转到 GameObject 菜单,并将鼠标悬停在灯光选项上以获取图 12.38 中显示的子菜单。

图 12.38:灯光选项
现在我们需要回到混合照明。我们已经讨论了这两种照明模式。有些游戏可能只使用烘焙照明,而有些游戏可能只使用实时照明。大多数游戏在 URP 中都会使用这两种照明。当你选择你创建的任何灯光时,检查器有一个选项可以选择实时、混合或烘焙。记住,烘焙意味着烘焙的间接光。混合照明最好的部分是它允许灯光在它所在的位置烘焙,但与非静态 GameObject 结合时表现出动态效果。这对于方向光很有用。这种光的作用就像太阳,所以我们希望它为静态物品烘焙,但对于角色或任何非静态物品则是动态的。你可以在下面的图 12.39 中检查器中看到这一点。

图 12.39:设置为混合的方向光
即使你已经设置了所有需要的静态网格,放置了灯光并将它们设置为实时、烘焙或混合模式,你仍然需要在灯光窗口中设置你的灯光设置。要到达那里,请参考下面的截图,如图 12.40 所示。

图 12.40:灯光窗口的路径
在弹出的窗口中,你将拥有几个可调的设置。这些设置将针对每个项目都是独特的。我们知道我们想要一些很好的阴影保真度。这意味着我们需要更多的样本和更高分辨率的灯光贴图。我们还将非常接近游戏中的角色,在电影场景中,这仍然是实时。在考虑你的设置时,需要考虑这些因素。你可能会提高设置,并使用巨大的灯光烘焙获得很好的阴影,但你的实时阴影可能无法处理它,并且会变得块状,这会使游戏感觉奇怪。考虑你的游戏将在哪个系统上播放,并在添加更多灯光和灯光贴图后彻底测试性能是个好主意。
有另一个工具可以在 Unity 中获取更精确的实时照明信息,而无需拥有大量的实时灯光。它被称为光探针。让我们看看这个工具。
光探针
创建光探针就像进入你的 Light GameObject 组并选择 Light Probe Group 一样简单。
你可以在上面的三幅图中的 图 12.38 中看到这一点。这个工具的作用是在 3D 中的点采样光线信息,这些点在 图 12.42 中显示。即使光线是烘焙信息,这些信息也会实时使用。如果你想要使用区域光(仅烘焙)的颜色并将其添加到角色上,这将非常有帮助。想象一下墙上的灯光,你不需要它投射阴影或实时。而不是资源密集型,你只需在该区域周围使用光探针,它将帮助实时捕捉非静态几何形状。
要设置这个,你需要手动放置光探针。资产商店上有自动放置它们的资产,但请记住,娱乐行业中任何自动化都需要艺术家的输入才能达到所需的经验。
光探针组在编辑组时,在检查器中看起来像下面的 图 12.41。

图 12.41:检查器中的光探针组组件
你可以添加、删除、选择全部,以及复制选中的内容。当你放置光探针时,只需知道它们是多个颜色位置的平均值。这些并不是一个区域光线的完美表示,而是一种近似,以提供额外的提升,确保游戏中的实时演员能够保持情绪。因此,添加探针直到它们形成一个漂亮的晶格。你拥有的越多,所需的计算能力就越大。对于每个项目,像往常一样,它将取决于系统来知道允许多少光探针以保持性能。
放置好它们之后,你可以按下播放并四处走动,或者只需将你的非静态 GameObject 在场景中拖动,以看到光线略微偏移。
在 图 12.42 中,这是我们的垂直切片初始走廊的光探针晶格的一个示例。

图 12.42:光探针晶格
这可能需要一些时间,并且将在放置你的灯光之后完成。如果你更改了你的照明配置,确保之后也重新考虑你的光探针。在我们开始抛光声音之前,还有最后一件事。我们想要回顾一下反射。
反射探针
世界中有一些材料会反射环境的颜色。这些是金属和/或光滑的材料。问题是,它们会反射什么?我很高兴你问了这个问题,因为 Unity 最初会创建一个仅包含天空盒的反射贴图,这样那些材料中就有东西可以反射了。你可以添加到场景中的另一个工具是反射探测器,它允许你指定一个区域,该区域具有该区域的反射数据。你也可以有重叠的体积。
这是一个有趣的问题,因为它并不是完美的表示,因为探测器的反射位置是从该探测器位置的中心开始的。如果你有一个很大的区域,你需要非常接近反射,同时还需要反射非常准确,你需要多个反射探测器,每个探测器的体积只需你需要的那么大。体积越小,反射图像越清晰。这些类型的事情直到你在这个世界上四处走动并寻找这个或处理你游戏的镜头,看到奇怪的反射,才会变得非常清楚。这里有一个小的注意事项;你可以创建实时反射,但它们非常昂贵。这些应该谨慎使用。直到我们都在家里有了量子计算机。
要创建反射探测器,选项与所有其他照明选项相同,在 GameObject 菜单下的照明中。
当你创建探测器并将其放置在你想要反射的位置时,你将需要使用检查器来编辑体积,它看起来就像下面的图 12.43。

图 12.43:检查器中的反射探测器组件
顶部中间的两个图标用于编辑和移动体积。选择点图标可以让你访问体积的形状,以便根据需要缩小和放大它。类型可以是烘焙的、实时的或自定义的。烘焙的只会烘焙一次,在运行时无法更改。实时的会随着游戏的每一帧运行而改变。自定义的允许你放置自己的自定义立方体贴图,而不是采样环境。如果你想在反射中扭曲环境,这可能很有用!立方体贴图设置用于调整立方体贴图的缩放和参数,以在性能成本增加的情况下提高所需的保真度。
最重要的设置之一是重要性设置!这个设置是一个整数,你将其设置为告诉游戏在存在重叠反射体积时显示哪个反射探测器。
这种方法的工作原理是,数字越高,重要性就越高。如果你有两个重叠的音量,比如洞穴入口处与洞穴外面的音量,那么你就可以将走廊设置为重要性级别 2。这样,当你进入重要性更高的音量区域时,反射探头会切换到它。这可能会在非常接近的反射表面上引起一些爆裂声。玩你的游戏时,注意它们在过渡时的反射。
添加整体照明是一项有趣的任务。它可以大大提高你游戏的可视质量,并且有一些很好的技巧可以设置它。接下来是声音润色。
声音润色
我们可以在游戏中做一些事情来使声音更加逼真。声音润色主要涉及调整声音的音量、更改最小和最大衰减距离,甚至替换那些你觉得听起来不好的声音。
这些都是在项目过程中已经调整过的内容。例如,在我们的第一个环境音中,我们可以调整音量或音调,看看什么感觉合适。或者我们可以更改衰减的最小或最大距离,添加我们可能遗漏的声音,确保某些更重要声音的音量比其他声音大,等等。
总体来说,混音和声音润色是一个反复迭代的过程,只是操纵数值,用其他声音替换声音,以获得最佳感觉。除非你将其放置在游戏中,否则你永远不知道一个声音会如何与其他声音搭配。
通过动画事件触发声音
我们想展示如何将声音添加到动画事件中。这个过程相当简单,因为我们已经知道如何添加动画事件,以及如何使用 AudioSource 组件触发声音。我们将为我们的角色行走添加脚步声。
首先,让我们选择我们的角色,MyvariWithCameraRig:

图 12.44:MyvariWithCameraRig
然后,让我们进入其子对象中,找到 SM_Myvari GameObject。在这里,你会看到动画组件!我们这里只需要几个东西。
首先,让我们创建一个新的脚本,命名为 AnimationSounds,然后我们将它放在我们的 Animator 组件 下方。之后,我们将添加 AudioSource 组件。它应该看起来像下面的 图 12.45:

图 12.45:SM_Myvari 检查器窗口
在我们继续前进之前,让我们在我们的 AnimationSounds 脚本中添加一个函数。移除 Start 和 Update 函数,并添加一个新的函数,命名为 PlaySound()。在这个新函数上方,声明一个新的公共变量,名为 public AudioSource AnimSound。

图 12.46:我们的新 AnimationSounds.cs
现在,在我们的 PlaySound() 函数中,让我们添加 AnimSound.Play()。
接下来,在检查器中,我们可以将AudioSource组件添加到AnimationSounds.cs组件的序列化字段中,并添加脚步声效果!

图 12.47:检查器中的 AnimationSounds.cs 脚本
太棒了!现在我们可以继续给动画添加事件标签。
为声音事件标记动画
我们在添加动画事件时遇到的一个大问题是,我们无法直接通过动画窗口添加事件,所以我们需要在 Unity 中打开 FBX 文件。
最好的方法是进入资产 > 动画并选择Myvari_Walk_Basic FBX。

图 12.48:Unity 中“资产”>“动画”文件夹的项目资源管理器
接下来,我们在检查器中向下滚动,直到我们到达事件下拉菜单。

图 12.49:动画剪辑检查器窗口
打开那个事件下拉菜单,也打开检查器底部的预览窗口。
它可能隐藏在检查器的底部,但你可以点击并拖动从底部将其拉上来!它应该看起来像图 12.50:

图 12.50:动画剪辑检查器窗口预览
接下来,使用预览上方的时间轴,我们可以循环到动画的不同部分。特别是,我们正在寻找放置脚步的地方,所以我们会想要找到这样的地方,脚接触地面:

图 12.51:动画剪辑检查器窗口预览
一旦你的时间轴对齐,就可以添加一个动画事件。然后在说函数的地方输入PlaySound——不要包括你之前看到的括号(在PlaySound()中)!由于某种原因,包括括号不会正确触发我们的函数。
这里就是我们放置事件的位置。

图 12.52:带有事件的动画剪辑检查器窗口时间轴
现在,当你进入游戏并四处走动时,你会听到声音!恭喜!我们现在有了脚步声!
随机声音
你可能会注意到我们的脚步声相当重复。这就是为什么我们经常喜欢在游戏中添加随机声音!
这是一个从音效池中随机播放的过程,这样声音就不会那么重复了!在这个例子中,我们有五种不同的脚步声效果可供选择,它们位于/Assets/Sounds:
MainFS_01.wav – MainFS_05.wav
接下来,让我们打开我们的AnimationSounds.cs脚本,看看我们如何添加随机声音。在这个例子中,我们将使用一个AudioClips列表,如下所示:

图 12.53:Animation.cs 公共列表 soundPool
然后,在PlaySound内部,我们将从这个列表中随机选择一个剪辑,并将其加载到我们的AudioSource组件中。我们将使用Random.Range来完成这个任务:

图 12.54:Animation.cs PlaySound()函数
接下来,让我们打开我们的AnimationSounds.cs脚本,看看我们如何添加随机声音。在这个例子中,我们将使用一个AudioClips列表,如下所示:

图 12.55:Animation.cs 在检查器中
就这样!我们现在正在从随机声音池中播放!
随机音调
有时,通过随机化音调甚至可以增加更多的变化。这个过程也非常简单。我们首先必须定义我们将要影响的音调范围。
我喜欢直接播放声音,然后调整音调来听听哪里听起来不错。打开包含我们脚步声的AudioSource组件,切换音调滑块!这将实时更新。

图 12.56:AudioSource组件
你会听到音调过高或过低会产生相当不真实的声音。所以我喜欢保持在 0.3 和-0.3 的范围内。在我们的代码中,我们只需在针对AudioSource组件的音调时添加一个简单的Random.Range()即可。

图 12.57:AudioSource组件显示如何实现随机音调
这就是我们需要的全部!在游戏的声音景观中创建深度最重要的方法之一是添加尽可能多的来源。添加像随机变化、动画中的小细节声音和动态音频这样的东西可以走得很远!继续玩游戏,听听你的变化。
摘要
本章介绍了我们在项目中使用过的许多不同工具。我们花了一些时间来回顾我们的最终艺术和资产处理流程。我们不仅关注模型和纹理,还检查设计,以确保每个资产都符合预期。在这个过程中,我们还介绍了从 Shuriken 和 VFX 图粒子系统中添加效果。这包括实现显示心灵感应效果的效果。
然后,我们讨论了光照设计。我们将 Unity 光照分解为光照贴图、反射、光照探针和烘焙。光照可以为游戏增添很多,所以这部分不应该被轻视!
然后为了完善我们的游戏,我们进行了声音优化,通过动画触发声音,并为声音添加随机性,以使游戏玩法更加生动。
这就是本书的全部内容!非常感谢您一直阅读到最后,并希望它为您提供了大量的知识。请考虑加入我们的 Discord 服务器,在那里我们可以回答问题,并更详细地讨论项目。
在此之后,有一个附加章节,介绍了更多可以用于不同项目的 Unity 工具,以及 Unity 为多人游戏、XR 和视觉脚本提供的一些产品。如果您也想要关于这些主题的书籍,请告诉我们!
第十三章:奖励:其他 Unity 工具!
在本书中,我们已经经历了一个第三人称环境解谜垂直切片。这是一个具有自身独特开发问题的特定游戏类型,这也意味着我们没有提及许多 Unity 的优秀工具,因此我们想用一小部分内容来谈谈 Unity 可以为您下一个项目提供哪些其他服务。我们将涵盖以下内容的入门级理解:
-
多人游戏
-
XR
-
机器学习代理
-
Bolt 视觉脚本
Unity Gaming Services
我们探索了一组名为 Unity Gaming Services 或 UGS 的工具。这些工具旨在提供需要大量时间开发且可以快速集成到您的项目中的解决方案。这些工具可以直接帮助设置多人游戏、XR(这是虚拟现实和增强现实的混合体)、视觉脚本(称为 Bolt),以及最终是一组创意工作流程工具。让我们首先看看可用的多人游戏工具。
多人游戏工具
Unity 提供了大量的服务,从关键工具和教育到成功地将多人游戏因素融入您的项目。在多人游戏类别中,Unity 将重点分为三个支柱:创作、连接和沟通。
创作
Unity 将其视为您游戏的基础,并引入了 GameObjects 的 Netcode 和实体的 Netcode 选项。实体的 Netcode 利用 Unity 的新数据导向技术堆栈(DOTS)。这个新的、高性能的、多线程的 DOTS 允许您充分利用多核处理器,以创建更丰富的用户体验,并使用易于阅读和在其他项目中重用的 C# 代码更快地迭代。DOTS 为程序员提供了一个直观的沙盒,用于创建安全的多线程代码,以实现一系列多人开发优势。
实体的 Netcode 利用 Unity 的新数据导向技术堆栈(DOTS)。这个新的、高性能的、多线程的 DOTS 允许您充分利用多核处理器,以创建更丰富的用户体验,并使用易于阅读和在其他项目中重用的 C# 代码更快地迭代。DOTS 为程序员提供了一个直观的沙盒,用于创建安全的多线程代码,以实现一系列多人开发优势。
专注于可流式传输的数据,DOTS 允许代码的重用灵活性,帮助他人理解它,并提高协作性。具备这些各种能力,DOTS 将现有的工作流程转换为转换工作流程。此工作流程只需一键即可将您的 GameObjects 转换为实体。在运行时,实体预览检查器可视化 DOTS 如何将您的 GameObjects 转换为实体。同时,使用 Unity Live Link 功能将允许您在播放模式下即时迭代,而无需每次都创建新的构建。无需创建新构建即可利用更快的迭代速度,让您和您的团队能够在目标设备上实时测试游戏体验。
Unity 为他们的可用资产堆栈创建了 DOTS 包。他们强烈建议您在此阶段使用预览包进行测试和项目的前生产阶段,因为它们会验证包以成为生产就绪。
连接
多人游戏中的用户体验通常与匹配、游戏前后的休息室以及排队时间相关联。Unity 通过休息室、中继、多人游戏和匹配器等服务开发了补充这些合作互动的功能。
Unity 休息室赋予玩家在游戏会话前后连接的能力。玩家可以使用简单的游戏属性创建公共休息室。这些休息室可以被其他玩家搜索、发现并加入。私人休息室允许玩家为仅限受邀嘉宾创建专属空间。
中继是 Unity 提供的一种服务到服务的工具,与休息室配合良好。当玩家从游戏中断开连接时,中继将自动移除断开连接的玩家,并通知您意外的断开连接。此外,您还可以通过 GameObject 的 Netcode 访问您合作游戏的可靠基础。
Multiplay 通过 Unity 提供了一种弹性、多云混合游戏服务器。这项服务允许您访问服务器托管和匹配,而无需构建和维护自己的后端基础设施。专注于提供更平滑的玩家体验,Unity 投资创建了超过 190 个数据中心,这些数据中心旨在实现可扩展的弹性和性能。这些服务器在全球范围内运行,维护着服务质量(QoS)标准。QoS 定位最佳匹配连接区域,无论玩家从何地游戏,都能为他们提供最稳定的连接,从而最大化玩家参与度,减少停机时间,并将新内容带给您的玩家,无论平台如何。
匹配器是通过与谷歌的开放源代码合作——Open Match 创建的。匹配器也是 Unity 专用游戏服务器托管解决方案 Multiplay 的一部分,为 Unity 的企业客户提供开箱即用的集成,可以扩展到各种玩家基础容量。匹配器包含开发者配置的匹配逻辑、可定制的评估器以及具有预定匹配功能执行的匹配循环,作为其托管解决方案,以在正确的时间和地点匹配您的玩家。
通信
当体验健康、沉浸且彼此之间有坚实的互动质量时,玩家参与度和留存率就会发生。Unity 通过 Vivox 和社区与安全提供玩家参与度工具,以支持积极的社交体验。
Vivox 是一个易于实现且可靠的特性,它支持功能丰富的语音和文本聊天服务,适用于你的游戏。Vivox 被行业领先的游戏如《 Valorant》、《英雄联盟》、《彩虹六号:围攻》和《绝地求生》所信赖,为玩家提供最佳通信服务。Vivox 可在任何平台、全球范围内以及任何游戏引擎上运行,可在两天内集成并扩展至数百万玩家。
即将推出的是 Unity 通信工具库的最新补充——社区和安全。Unity 在这个平台上的重点是玩家安全分析和游戏管理。
XR 插件
XR 是一个包含以下应用的通用术语:虚拟现实(VR)和增强现实(AR)。VR 为用户创建一个完全围绕其用户的环境。AR 通过技术设备的镜头在现实世界之上叠加数字内容。XR 将用户的现实世界和数字世界结合在一起,以实现相互交互。
Unity 开发了一个名为 XR SDK 的新插件框架。这个框架使得 XR 提供商能够成功集成到 Unity 引擎及其所有功能,以优化之前提到的每个应用程序。
Unity 支持以下 XR 平台:
-
ARKit
-
ARCore
-
微软 HoloLens
-
Windows 混合现实
-
Magic Leap
-
Oculus
-
OpenXR
-
PlayStation VR
Unity 目前不支持 WebGL 上的 XR。
这种基于插件的方案允许 Unity 快速修复错误,从平台合作伙伴那里分发 SDK 更新,并支持新的 XR 设备,而无需修改核心引擎。
机器学习代理
Unity 理解,人工智能(AI)研究的进步取决于使用当前基准训练 AI 模型时解决现有环境中的难题。当游戏变得复杂时,开发者将需要创建智能行为,这可能导致编写大量使用高度专业工具的代码。
Unity 创建了一个机器学习代理(ML-Agents)工具包,这意味着你不再需要“编码”涌现行为,而是通过深度强化学习和模仿学习相结合的方式,教会智能代理“学习”。
因此,这允许开发者创建更加物理、视觉和认知丰富的 AI 环境,以及更具吸引力的游戏玩法和增强的游戏体验。
Bolt 可视化脚本
使用交互逻辑可视化地开发游戏玩法机制。Bolt 允许你创建基于视觉、图形化的系统,而不是编写传统的代码行。通过可视化脚本,Bolt 促进了设计师、艺术家和程序员团队之间的无缝协作,以实现更快的原型设计和迭代。
更多的技术团队成员可以通过自定义节点来赋能非程序员,无论编程知识水平如何,都能提高生产力。Bolt 提供了流程图、状态图、实时编辑、调试和分析、代码库兼容性和易用性等功能,以下我们将逐一介绍。
流程图
流程图将是您项目中交互的主要工具。使用基于节点的动作和值,这些图将允许您以任何指定的顺序指定逻辑。
状态图
状态图允许您创建自包含的行为,指示对象在达到特定状态时应执行哪些操作。状态图与高级逻辑兼容,如 AI 行为、场景或关卡结构,或任何在状态之间转换的行为。
实时编辑
实时编辑可以在播放模式下的图中实时进行。快速迭代和测试想法,无需重新编译项目更改。
调试和分析
调试和分析可以在播放模式下的视觉脚本图中找到。Bolt 会突出显示正在执行的节点,然后,如果发生错误,节点将很容易被识别。
代码库兼容性
代码库兼容性允许在您的图中使用任何第三方插件或自定义脚本。通过反射直接访问您的代码库,视觉脚本始终是最新的,无论它是一个方法、字段、属性还是来自 Unity 的事件。
易用性
易用性视觉脚本旨在通过用户友好的功能、注释和可搜索功能,使非技术创作者能够访问。
摘要
本章的目的是展示 Unity 可以在您的下一个游戏设计冒险中为您提供的一些功能。它们在我们的项目中没有使用,这就是为什么我们将它们留在了附录章节中。多人游戏工具是制作多人游戏的一个极好的资源。通过将 AR 和 VR 插件整合到一个 XR 插件中,我们现在有一个很好的、集中的插件,可以用于所有改变现实,无论您使用的是哪种硬件。然后,为了结束这本书,我们讨论了 Bolt 的视觉脚本。如果您觉得编程不容易上手,可以考虑看看这个。
我们希望您从这本书中学到了一些东西,并且在阅读过程中享受了乐趣。我们在本书开头和整个过程中提到,如果您想与其他购买了这本书或其他 Packt 出版的以 Unity 为中心的书籍的人互动,Discord 社区是一个好地方。我们很乐意在那里见到您,并帮助您实现下一个伟大的想法。



浙公网安备 33010602011771号