Hi,我是李大数,喜欢探究数学,美术,游戏,心理等一切有艺术感之物,并把所得、所知、所感和朋友们一起分享的李大数。
前言
Unity官方出品的《Ruby的大冒险》2D游戏辅导课确实是不可多得的很棒的入门课程,作者不是像很多“傲娇”的开发者一样寥寥数语讲几个要点,而是像一位耐心和蔼的老师一般,一步一步由浅入深的讲解每一个具体的操作和背后的原理,最终完成一个非常完整的具备2D游戏所有开发要素的游戏。可惜只提供了英文版本,中文读者难免在学习过程中产生种种误读而踩坑,因此我计划逐课进行解读,加入自己的一些补充性总结和实际操作经验。
各位读者如果发现错误或者有任何建议,可在评论中提出,并说明版本号,我会持续修订此文。
版本说明
v0.1:2021年2月15日
- 完成约70%的工作
v0.2:2021年2月16日
- 第一节课的缺失内容补充完整
v0.3:2021年2月17日
- Asset翻译为“资源”不是很准确,斟酌后改为“资产”
- 第十四课的缺失内容补充完整,并添加了使用中文字库的方法
文章目录
- 前言
- 版本说明
- 术语中英对照表
- Ruby的大冒险(2D游戏精品辅导课)
- Lesson1 设置Unity编辑器
- Lesson2 主角和第一个脚本
- Lesson3 角色控制器和键盘输入
- Lesson4 用Tilemaps设计游戏世界
- Lesson5 装饰游戏世界
- Lesson6 世界交互 - 阻止移动
- Lesson7 世界交互-可收集物品
- Lesson8 世界交互 - 伤害区和敌人
- Lesson9 精灵动画
- Lesson10 世界交互 - 子弹
- Lesson11 摄像机 - 电影模式摄像机(李大数注:后文简称电影机)
- Lesson12 视觉设计 - 粒子
- Lesson13 视觉设计 - UI - Head Up Display
- Lesson14 世界交互 - 对话框和射线检测
- Lesson15 音频
- Lesson16 编译、运行和发布
术语中英对照表
- 本文内容为中英文混合,读者如有不明,请参考此对照表
- 所有涉及的Unity界面文本均使用英文,以便读者实际操作时寻找
EN | CN |
---|---|
Asset | 资产 |
Animator | 动画器 |
AnimationClip | 动画剪辑 |
Prefab | 预制件 |
Sprite | 精灵 |
GameObject | 游戏对象 |
Tilemap | 瓦片地图 |
Rigidbody | 刚体 |
Collider | 碰撞体 |
Trigger | 触发器 |
Anchor | 锚点 |
Pivot | 支点 |
KISS | Keep It Simple and Stupid |
Audio | 音频 |
Audio Clip | 音频剪辑 |
Texture | 纹理 |
Script | 脚本 |
PPU | Pixel per Unit,每单元像素 |
Ruby的大冒险(2D游戏精品辅导课)
Lesson1 设置Unity编辑器
欢迎来到Ruby的大冒险:2D游戏初学者项目!
在这个项目中,你不仅能学会如何创建游戏——每节辅导课都会解释每步操作背后的逻辑规则。不需要先具备什么经验,所以这个项目非常适合于开始你的Unity之旅。
在这第一节课中,你将探索Unity编辑器,并学会为你的游戏导入Assets资产。
1. 更新你的Unity版本
在下载本节课的项目之前,你首先需要做的是:
关于使用Unity Hub,参见Unity Hub 文档
如果你没有最新的官方release版本,从Unity Hub窗口左侧的菜单中选择Install 一栏来下载它。
注意: 本项目的学习内容仅兼容于2018.3以及之后的官方release版本。
2. 下载项目
-
打开Unity Hub。
-
选择窗口左边的 Learn 一栏。
-
在可用项目列表中向下滚动找到此项目并选中。
-
在项目对话框窗口中,选择 DOWNLOAD PROJECT。
-
下载完毕后选择 OPEN PROJECT 。
Unity将会导入项目,同时更新必要的开发包。 -
在顶部菜单中,选择 File > Save来保存你的项目版本。你也可以使用Ctrl + S (Windows) 或者 Cmd + S (macOS)快捷键来保存。
-
命名并保存你的场景。
3. 重要:不能保存项目时的处理方式
注意:一些用户会碰到无法保存所下载的项目的问题。如果你也遇到这个问题,你可以尝试以下办法:
-
在顶部菜单中,选择 File > Close。
-
在询问是否保持项目的对话框中,选择Keep。
-
使用你的操作系统的保存窗口保存你的项目。为你的项目选一个容易访问的位置,例如一个专门的项目目录或者你的文档目录。
-
当项目被保存时,重新打开Unity Hub。
-
选择Add然后导航至你保存的项目位置。你的项目就可以添加到Hub中的项目列表中了。
-
在项目列表中左键单击项目来打开它,继续我们的课程。
完成上述过程后你将可以正常保存项目了。
4. 另一种导入方法:使用Unity资产商店
在你探索Unity编辑器之前,让我们先得到你项目中需要的资产文件。你可能已经通过上面的第二步搞定了,不过你仍可以利用Unity资产商店。
所有的文件都在Unity资产商店里,该商店让资产创作者能够提供工具或者文件给其他Unity用户。
- 要访问Unity资产商店,打开编辑器,选择菜单 Window > Asset Store。
然后资产商店就在你的编辑器内打开了。
要加载资产到场景:
- 在搜索条中,输入“2D Beginner: Tutorial“然后点击搜索结果。
- 在“2D Beginner: Tutorial Resources”页面,点击下载然后等待下载完成。
- 点击Import。这会打开导入Unity Package 窗口,该窗口列出了所有文件。
- 点击Import把文件导入你的Unity项目。
现在你已经设置完成,让我们开始探索编辑器是如何工作的!
5. Unity编辑器的各个窗口和视图介绍
让我们首先概览编辑器的窗口。别担心,你无需记住每个细节!
这里仅仅给你一个哪个窗口做什么的宽泛概念,这样当你探索编辑器更多细节的时候,你就知道该去看屏幕上的哪一部分了。
Project window(项目窗口)
视频
项目窗口列出了你当前项目中所有的文件和目录。这些文件包括所有你项目中用到的图片,声音和3D模型。他们以Assets(资产)的形式被集合于此。
- 打开Demo folder(演示目录),双击DemoScene(演示场景),这将打开一个场景,你可以把它作本次辅导课剩余部分的一个演示。
Console windows(控制台窗口)
视频
让我们看看控制台窗口。缺省布局下,控制台窗口的tab栏紧贴项目窗口的tab栏。
控制台窗口显示了游戏给出的警告和错误,为修正这些问题提供有用的信息。
你可以通过拖放控制台窗口的tab栏,把它停靠在项目窗口的旁边。
Hierarchy(层级窗口)
在Unity中,你的游戏由场景组成。可以认为场景是一个平面或是一个不同的环境。每个场景内有一个对象列表,这些对象在Unity内被叫做GameObjects。
你可以放置GameObject(游戏对象)到一个父子关系的层次结构中。游戏对象可以是其他游戏对象的孩子,这让你可以成组移动它们(意思是如果一个父对象移动了,该父对象的所有孩子对象也会跟着移动)。层级窗口在你的场景中以父子关系展示了所有的游戏对象。
Scene view(场景视图)
视频
场景视图是一个实时预览窗口,它可以实时预览当前载入的场景及其层级结构中的所有游戏对象。你可以用它在场景中放置和移动游戏对象。在场景视图中点击游戏对象可以让它在层级窗口中高亮显示。
Game view(游戏视图)
游戏视图是当你在编辑器中测试游戏时显示的一个视图。
视频
游戏视图缺省不显示,只有点击与场景视图相邻的tab栏才会显示。与场景视图允许你移动游戏对象和观察整个场景不同的是,游戏视图展示玩家将会看到的内容,可能只是摄像机范围的一部分(摄像机范围在场景视图由有一个白色方框表示)。
Inspector windows(属性窗口)
当你在层级窗口或者项目窗口中选择一个条目时,属性窗口会展示其所有相关数据项。
视频
GameObjects(游戏对象)
对于场景中的游戏对象,属性窗口显示游戏对象的数据。
Unity使用了一种Object - Component(对象 - 组件)的模型,也就是说你的场景由添加了各种功能组件的游戏对象而组成。
举个例子,一个精灵渲染器组件在场景中游戏对象的位置显示了一幅图片,一个音源组件在场景中游戏对象的位置播放了一个声音。
所有游戏对象都有一个Transform component(变换组件),该组件使你可以指定对象在场景中的位置和旋转角。所有其它的组件是可选的,你可以按需添加。
Assets(资产)
对于资产,属性窗口显示Unity用到的导入设置。本系列辅导课将着重说明和解释用于2D游戏的资产的导入设置。
6. 工具栏和导航UI
工具栏
工具栏包括最常用的工具按钮来帮你设计和测试游戏。
Play buttons
Play
播放按钮用来测试你当前加载到层级视图中的场景,这样你就可以在编辑器中实时试玩你的游戏了。
Pause
暂停按钮,就像你猜到的,让你可以暂停游戏视图中的游戏。它帮你发现你看不到的界面问题和游戏bug。
Step
单步按钮用来在暂停状态下逐帧前进。此功能在你寻找游戏中的实时变化时非常有帮助。
Manipulating objects(操作对象)
这些工具移动和操作场景视图中的游戏对象。点击以激活它们。
下表展示了每个工具用到的快捷键:
另一个好用的快捷键需要你记住:
- F - 聚集与选中的对象。如果你忘记当前场景中选择了哪一个对象,只要在层级窗口中选中它,然后按下F键,它就会在场景视图居中显示。
鼠标导航
在场景视图中,你可以使用如下鼠标功能:
- 左键单击可以在场景视图中选择游戏对象
- 中键单击且拖拽,可以移动场景中的摄像机。
- 右键单击且拖拽,可以场景视图中的摄像机。
- 上述是2D模式下的操作,3D略有不同,请读者自行尝试。
更多在场景中移动游戏对象的方法,参见:场景视图导航
7. 布局
缺省布局
你能以多种方式布局你的Unity编辑器。每种布局自有其优点,你可以找出最适合你的。
这里概览一下每种布局看看哪些可用。
要切换布局,选择 Window > Layout (或是使用 编辑器右方角的Layouts 下拉菜单)。选择下列布局的一种:
此处的设置因人而异。对于这个Ruby 2D 角色扮演项目随后的课程,我们会使用缺省布局,同时控制台窗口停靠在项目窗口旁边,就像你在辅导课中控制台那一节中看到的。整个辅导课中我们都将使用这个布局。
8. 小结
现在你已经初步了解了Unity编辑器是如何组织的。如果在随后的课程中提到“项目窗口”,你应该已经知道在哪里找到它了。一些资产已经添加到你的项目来帮你打造整个游戏。
在下一课中你可以开始创建你的游戏了。创建一个新场景,添加一幅图片给它,然后写下你的第一个脚本来让图片动起来。
李大数小结
本节课虽然不是核心课,但却是极其重要的基础课。不夸张地说,很多游戏开发环境的安装没有任何愉悦感,而是令人充满了沮丧,下载了A包,发现又要依赖B包,下载了B包又发现操作系统版本不对应,面对这种地狱般的感受,90%的同学直接放弃,另外10%的同学在踩了无数坑后放弃。而Unity的安装过程与之相比就非常人性化了,先安装Unity Hub,然后从下载开发平台到打开样例项目都会比较流畅(前提是网速够快),再加上本节课一步一步的贴心指导,只要严格遵守,那更是全程无痛无坑。
本节课要掌握三个要点:
- 第一个要点是Unity的下载和安装。
- 第二个要点是Unity资产商店的使用,里面很多收费或者免费的资产值得我们去精挑细选。
- 第三个要点是Unity的主要窗口的功能。在这里,最重要的五个窗口(上三下二)是必须要掌握的。“上三”是指层级窗口、场景视图、属性窗口,“下二”是指屏幕下方的项目窗口和控制台窗口。可以这样来概要理解(并非绝对),“上三”窗口是动态的,表达了游戏运行时的所用到的对象的组织结构和可视化。“项目”窗口是静态的,是硬盘上一个个物理存在的文件,表达了游戏项目在开发过程中的磁盘文件组织结构。“控制台”窗口是用来发现错误和查看游戏运行时的日志的,后边会用到。
Lesson2 主角和第一个脚本
上一节课中,你已经学到了Unity编辑器的布局,了解了一个场景有游戏对象组成,而游戏对象上的组件决定了这些游戏对象如何在我们的游戏里运作。
现在是时候在你自己的场景中添加一个游戏对象了!
材料
Ruby.png
1. 创建一个新场景
让我们开始创建一个工作用的新场景。
-
选择 File > New Scene。当然,你也可以使用快捷键 Ctrl + N(Windows/Linux)或者 Cmd + N(macOS)。
-
如果一个弹出窗口出现,提示未保存的修改,这是因为在演示场景中你移动或者改变了一些东西,Unity要确保你愿意放弃这些修改。在这里我们不做保存。
-
现在你有一个名为“Untitled”(未命名)的空场景 。这意味着场景还没有被保存到磁盘上。
让我们用一个合适的名字保存。选择 File > Save 或者使用快捷键 Ctrl/Cmd + S。 -
选中名为场景的目录,并把你的场景命名为“MainScene”(主场景)。
现在你已经有了一个工作场景,你可以在剩下的课程中使用此场景。记住经常用快捷键Ctrl/Cmd + S 来保存你的修改到磁盘。那样做了,如果你退出Unity,后边再返回,你也不会丢失任何修改。
2. 导入资产
3. 用Sprite创建GameObject
4. 设置角色坐标
5. 距离的单位
- unit在不同的游戏中有不同的值
- 蚂蚁世界游戏的one unit可以为1cm
- 人类世界游戏的one unit可以为10m
6. 创建脚本
- 在Assets目录中创建Scripts目录
- 脚本命名为游戏对象名加上Controller,例如RubyController
7. 缺省脚本详解
- 双击打开
- using xxx;是导入相应的库
- Start()在游戏启动时调用一次
- Update()每帧调用,典型帧率为30次/秒或60次/秒
- 在Update函数中可以移动角色,读取玩家输入等
8. 调整Update函数
public class RubyController : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
Vector2 position = transform.position;
position.x = position.x + 0.1f;
transform.position = position;
}
}
- 保存后生效
9. 变量声明
10. 移动游戏对象
11. 存入游戏对象的新位置
12. 在Unity中使用代码
13. 写脚本
14. 写脚本
15. 写脚本
16. 检查脚本
17. 小结
李大数小结
本节课主要掌握三个要点:
第一个要点是理解游戏对象(GameObject),Unity场景中万物皆GameObject,相机、精灵、网格都是。GameObject可以有子GameObject,子子孙孙,无穷尽也。从而以场景为根,形成了一棵有层级的树,而树中每个节点都是GameObject。精灵就是一个特殊的GameObject,它有个
第二个要点是理解游戏世界的坐标系统和坐标单位,Unity的游戏世界是一个左手坐标系,所谓左手坐标系,就是伸出左手拇指,蜷曲的四指从x轴正向指向y轴正向,则拇指所指方向为z轴正向,这种方法也称左手定则。对于2D游戏来说,使用左手定则可知,x从左到右、y轴从下到上,则z轴就是从屏幕外向屏幕内,所以从我们的眼睛出发,z坐标越小的物体越在前台越看得到,z坐标越大的物体越在后台越看不到。游戏世界的坐标单位不是一个具体的长度,而是以“Unit”为单位,这个单位可能是1cm,也可能是10m,它和设备的分辨率无关,而和我们游戏的内容相关。
第三个要点是理解脚本,脚本是精灵的行为逻辑控制部分,以组件的形式存在于精灵中,类似于C++、Java等面向对象的语言中对象所对应的类定义。不过这个脚本可没有那么自由,要通过几个回调函数来实现逻辑控制(所谓带着镣铐起舞)。几个典型的回调函数如Awake、Start、Update、FixedUpdate等,其回调时机各有不同。
Lesson3 角色控制器和键盘输入
1. 用键盘输入来控制角色移动
- 使用Unity Input System
2. 查看缺省的输入设置
- Go to Edit->Project Settings, 选择Input页面
- 在Input页面上列出了轴域的所有设置
- 例如手柄上的摇杆,水平轴可能为如下值
- 左为-1
- 中间为0
- 右为1
- 对于键盘按键,该轴被定义为2个键值
- 反向的key在按下时为-1
- 正向的key在按下时为1
- 可以看到设置中的negative button是left键,positive button是right键
3. 修改代码以使用轴域
- 双击打开RubyController脚本
- 修改Update()函数
public class RubyController : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
float horizontal = Input.GetAxis("Horizontal");
Debug.Log(horizontal);
Vector2 position = transform.position;
position.x = position.x + 0.1f * horizontal;
transform.position = position;
}
}
- 保存
4. Review代码的改变之处
5. 写log到控制台
6. 调整移动的代码
7. 即时测试
8. 尝试写纵向移动代码
9. Review代码
10. 时间和帧率
- 在Start()函数中设置帧率
void Start()
{
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = 10;
}
11. 用Units/秒来让Ruby移动
- 上节的方法实际上是使用Units/帧来计算每帧位移,缺点很明显,随着帧率不同,相同Input下每秒移动的位移也不同
- 本节使用Units/秒来计算每帧位移,不管帧率如何,相同Input下每秒移动的位移永远相同
- 注意用户Input的horizontal决定了速度在水平轴分量的方向,用它乘以速度大小Units/秒,即为真实水平速度,然后与Time.DeltaTime(这一帧与上一帧的浮点秒数时间差),得到这一帧的水平轴位移(可正可负)。
1.在编辑器中如下修改脚本
Public class RubyController : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
//QualitySettings.vSyncCount = 0;
//Application.targetFrameRate = 10;
}
// Update is called once per frame
void Update()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector2 position = transform.position;
position.x = position.x + 0.1f * horizontal * Time.deltaTime;
position.y = position.y + 0.1f * vertical * Time.deltaTime;
transform.position = position;
}
}
2.保存
12. Review脚本
13. 测试脚本
14. 检查脚本
15. 小结
- 本课学习了用户输入的角色移动的相关概念
李大数小结
本节课要掌握三个要点:
第一个要点是理解Unity对于用户输入所抽象出的概念,例如轴域的取值为-1、0、1,代表移动的方向是反向、静止、正向。
第二个要点是理解如何设置一个合理的移动速度,应当使用“点/秒”而不是“点/帧”。
第三个要点是掌握写日志到控制台的方法,这对游戏的开发除错非常重要。
Lesson4 用Tilemaps设计游戏世界
1. 介绍Tilemaps
- 为了节省时间而出现
2. 创建一个Tilemap(瓦片地图)
- 在Hierarchy窗口,右击空白区
- 在弹出菜单中选择2D Object > Tilemap
- 这个操作会Create两个GameObject在层级窗口中
- Grid(网格,可以想象为画板)
- Tilemap(瓦片图,可以想象为画纸)
3. 创建一个新Tile(瓦片)
Tilemap不能直接使用精灵,而是使用瓦片,所以要创建新Tile:
- 在项目窗口中,导航到Assets > Art
- 创建目录“Tiles”
- 右击,在弹出菜单中选择 Create > Tile,在对话框中把Tile命名为“FirstTile”并保存
- 此时查看FirstTile的属性,会发现它的属性Sprite为空
4. 给FirstTile赋予一个Sprite
- 在本教程顶部的辅导材料中下载Tile.png
- 存入Sprites目录
- 把这个Sprite拖入FirstTile的Sprite属性栏
5. 把FirstTile这个瓦片加入Palette(调色板)
隐喻:Grid是画板,Tilemap是画纸,Tile是颜色,Palette是调色板
- Go to Window > 2D > Tile palette,打开调色板窗口
- 选择Create New Palette,出现用于设置Palette的窗口
- 把新的调色板保存到Tile目录
- 把FirstTile拖入调色板窗口
- 选中Brush工具
- 在场景视图的网格中绘画
- oops!出现了空隙!
6. 让你的瓦片精灵适合于网格
网格和瓦片以Unit为通用单位,由于此时网格的每单位像素(Pixel Per Unit,简称PPU)是100,而瓦片Sprite纹理的实际Cell Size是64x64,所以在SceneView中的Grid中出现了空隙。
- 在瓦片Sprite的属性窗口中把PPU从100改为64,点击Apply,此时会发现Grid中空隙消失了。
7. 什么是Tileset(瓦片图集)
上面讲述了用单个Tile来组成Palette的功能,Palette更强大之处是可以用Tileset(瓦片图集,含有多个Sprite的纹理图片文件)来一次生成多个Tile,通过查看Tile文件信息可以看出,Tile通过m_Name和guid来引用Sprite
- Tileset是一个纹理图片文件
- 通过这个文件可以一次创建多个Sprite
8. 调整瓦片图集
本节介绍了Sprite编辑器的使用
9. 调整瓦片图集的Sprite设置
本节介绍了如何把图片切成9个Sprites
10. 用瓦片图集精灵批量生成瓦片
把Tileset图片拖放到调色板编辑器即可
11. 调整其它瓦片图集
12. 绘制瓦片地图
- 调色板编辑器和精灵编辑器都可以停靠在属性窗口内,比浮动模式好用
- 熟练掌握并理解工具栏的功能
13. 绘制瓦片地图
准备好了吗?开始绘制你的游戏世界!
眼下你不必绘制白色矩形框外部分。这个矩形框就是你的相机可视部分,也就是你在游戏视图中可以看到的部分。在以后的辅导课中,你会学会如何移动相机。
当你完成瓦片地图绘制时:
- 保存Scene
- Play测试一下
- 对于纯2D游戏,我们尽量不修改Z坐标,而是用其它方式来处理遮挡关系
14. 改变Tilemap的“Order in Layer"
- 相机从正无穷看向负无穷
- 所以Tilemap的Order in Layer典型值可以设置为-10
15. 小结
在本课中,我们学会了
- 用瓦片来绘制瓦片地图
- 用精灵来创建瓦片
- 用单个图片文件创建多个精灵
李大数小结
本节要掌握三个要点:
第一个要点是整体把握瓦片地图涉及的所有概念和它们之间的关系。瓦片地图包括网格、瓦片地图、瓦片、调色板,以及之前学过的图片和精灵这些概念。网格可以有多个瓦片地图,瓦片地图由瓦片和调色板一起组成。每个瓦片都要有一个精灵,一幅图片可以切分为多个精灵。
第二个要点是导入图片和瓦片地图的PPU设置。
第三个要点是批量生成瓦片的快捷方法。
Lesson5 装饰游戏世界
1. 添加装饰品
- 在工程窗口中,导航至 Assets > Art > Sprites > Environment目录,选择MetalCube Sprite
- 把MetalCube拖到层级窗口以添加到场景中
- 使用Move Tool(快捷键:w)来放置金属箱到场景中到合适位置
- 保存场景
- 播放场景,绕着箱子移动角色
此时,我们会发现一个问题,角色可以自由穿过箱子,并且始终位于箱子的前方或是后方。
2. 如何修复顺序问题?
在这个游戏中我们想要的效果是越在方(y坐标越小)的物体越优先看到。
3. 改变Graphics设置
- 导航至Edit>Project Settings
- 左侧选中Graphics
- 找到 Transparency Sort Mode这个字段,改为Custom Axis,
- 按x=0,y=1,z=0设置,让相机居于y轴从负无穷看向正无穷
- 关闭Project Setting 窗口,保存我们的修改
- 看起来还行,但仍有问题,角色看起来突然从箱子后面跳到了前面,此时我们需要Sprite Renderer组件
4. 调整Sprite设置
本节要掌握Sprite的 Sprite Sort Point这个属性,这个属性决定了用哪个点的坐标y值来排序,属性可选项有两个,center中点或者pivot轴点。
- 中点固定是在(0.5,0.5)处
- 轴点是可变点,在图片导入属性中设置,缺省为中点,也可以在Sprite编辑器中自由定制数值
注意当两个GameObject都使用center作为排序点时,当角色GameObject移动使得二者重合时,就会发生排序变化,导致后台的GameObject突然跳到前台,为了避免这个bug,必须小心的设计GameObject的排序点。例如纵向排列的角色和一个屏风,从现实世界讲,只要角色的脚在屏风下边沿上方,角色就应该被屏风遮挡。而角色的脚在屏风下边沿下方时,角色就应该完全出现。所以此时二者的排序点都应该选pivot类型,并把二者的pivot设置为bottom。
对于本例子中的Ruby和金属箱,也是一样的道理,同样应该把二者的pivot都设置为bottom。
5. 调整单个Sprite的轴点
6. 使用Sprite编辑器修改轴点
7. Prefab(预制件)是什么?
Prefab是一个持久化的GameObject文件,编辑器中使用时被加载为一个起模版作用的GameObject,可以用它来创建新的GameObject。
8. 创建一个预制件
- 在项目窗口中,导航至顶级目录(Assets目录)
- 创建新目录Prefabs
- 从层级窗口中拖一个GameObject到这个目录,即可生成一个Prefab
- 此时,把这个Prefab拖到层级窗口或者场景中即可生成一个新GameObject
9. 调整预制件的设置
- 层级窗口中的源自预制件的GameObject为蓝色,并且右侧有个小箭头
10. 创意时间
现在你可以添加自己的装饰品了,在Art>Sprites>Environment目录中你可以找到很多,比如房屋,树木,井盖等。
导入这些资产,记住以下几点:
- 把Pivot移到bottom
- 调整PPU的值
- 使用Prefab来创建GameObject实例
- 使用Order in Layer来控制遮挡关系,例如井盖应该总在最后。
11. 小结
预制件非常重要!!!
李大数小结
本节课要掌握三个要点:
第一个要点是如何控制显示次序,2D游戏不通过Z坐标来修改,而是通过排序层和层顺序号来控制,这两个值均为相机位于正无穷看向负无穷。
第二个要点是理解精灵的支点的概念,所有的缩放,旋转,移动都是基于支点来进行的,这是一个规范化坐标,也就是0~1之间的值。
第三个要点是掌握预制件的使用,这可以大大加快游戏UI的设计,并且很方便调整同类对象的属性。
Lesson6 世界交互 - 阻止移动
1. 物理系统是什么?
- 重力(gravity), 下落的力
- 动力,推动物体前进的力
- 摩擦力
Unity内置了物理系统以计算物体的移动和碰撞。为了提高性能,Unity只对有Rigidbody(刚体)2D组件的GameObject进行计算。
2. 添加刚体2D组件
把Ruby Sprite拖放为预制件,在预制件中添加刚体2D组件
3. 禁用重力
注意由于重力缺省作用于y轴,所以此时加入了刚体2D的GameObject会自动掉落,由于这个游戏实际上是“顶视图”,所有物体都是大地,所以要禁用其重力,方法是把刚体2D属性中的重力Scale设置为0.
4. 禁用Ruby预制件的重力
5. 什么是Collider(碰撞体)?
碰撞体是类似方或圆的形状,用于物理系统进行碰撞计算。
6. 添加碰撞体到GameObject
为Ruby和箱子添加碰撞体之后,Ruby发生来颤抖和旋转现象。
7. 修复Ruby的旋转
首先,告诉“物理系统”不要旋转GameObject,尽管现实世界的碰撞会导致GameObject发生旋转,但是2D游戏不需要这个功能。可以在刚体2D组件的约束属性里冻结某个轴的旋转。
8. 为何Ruby会颤抖?
用户让角色先行移动,物理系统计算出发生碰撞,又把角色反向推回,如此反复,就发生来角色颤抖。
9. 修复Ruby的颤抖
解决方法是让物理系统在角色真正移动之前就计算出发生来碰撞从而阻止角色移动。
不要直接把新位置赋值到transform,而是赋值到刚体,不管能否移动,物理系统会在计算过后把刚体位置同步到transform。
10. Review修改之处
11. Resize碰撞体
12. 为瓦片地图添加碰撞体
添加的是“Tilemap Collider 2D”组件。
13. 优化瓦片地图的碰撞体
添加Composite Collider 2D(组合式碰撞体2D),此时会自动添加一个Rigidbody 2D组件。
14. Check脚本
15. 小结
李大数小结
本节课要掌握两个要点:
第一个要点是物理系统的使用,为什么会出现角色颤抖和旋转的问题?如何解决这些问题。
第二个要点是瓦片地图如何使用碰撞体。
Lesson7 世界交互-可收集物品
既然你已经精心装饰了你的世界,并且和角色有了更好的交互性,是时候增加更多的可玩性了。
本节课中,你会学到另一个重要的游戏元素:触发器,它让角色可以收集混在其它物体之中的对象。
1. 给Ruby添加一个健康状态(李大数注:为符合常见的中文表达,后文简称血量)
我们来示范一个收集健康包(李大数注:后文简称食物)来恢复主角血量的例子。不过在此之前,你需要修改角色的脚本来添加血量给它。
- 打开RubyController脚本。
- 如下修改脚本:
代码略,接下来的小节详细注解。
2. 创建新变量
首先,创建了两个新变量:
public int maxHealth = 5;//公共变量,表示主角的最大生命状态。整型,显式设置为5.
int currentHealth;//不加修饰的变量作用域为私有,即本类内方可访问。整型,隐式设置为0.
maxHealth是主角的最大健康值,currentHealth是主角的当前健康值。
3. 在游戏启动时设置当前血量为满值
在Start函数中
currentHealth = maxHealth;
4. 添加一个函数来修改健康值
void ChangeHealth(int amount)//函数返回值 函数名(函数参数)
{
currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);//计算新的健康值
Debug.Log(currentHealth + "/" + maxHealth);//日志输出健康值
}
5. 在Unity编辑器中检查修改之处
此刻选中场景中的角色,你会在属性窗口中的RubyController组件中看到一个新属性:
Max Health,其值为5,和我们的代码完全一致。可是我们分明定义了两个变量,为何这里只显示了一个?原因很简单,这两个变量的作用域不用,maxHealth是public型的,可以从外部访问,也就是可以从Unity编辑器访问,而另一个是private私有的,Unity编辑器无法访问,也就无法在属性窗口中显示了。
6. 小练习:暴露另一个变量
记得在变量声明时加public前缀即可。
7. 什么是Trigger(触发器)?
既然你已经为主角添加了Health(李大数注:血量),下面让我们添加一种获取血量的方式。
为此,你需要使用触发器。触发器是一种特殊的碰撞体,它不会阻止移动,但是物理系统仍然会检查角色是否和它们发生了碰撞。角色进入一个触发器后发送一个消息给你,以便你处理该事件。
8. 创建一个可以收集的“Health”游戏对象(李大数注:食物之意)
让我们开始创建一个可以收集的食物:
- 首先,重复所有创造金属箱子的步骤
- 在项目窗口中,导航至 Assets > Art > Sprites > VFX 找到CollectibleHealth。
- 在场景中导入它,调整PPU为正确的大小
- 添加一个Box Collider 2D到新的GameObject,缩放它以更好的适配精灵。
- 现在你可以点击Play,Ruby 将会碰撞可采集的康复包就像碰到箱子一样,这可不是我们想要的。
- 退出游戏模式。
- 在属性窗口中,找到Box Collider 2D组件,选中Is Trigger复选框。
- 现在再次测试游戏,角色将会穿过康复包。物理系统通知了碰撞,但是由于没有处理代码,我们的游戏并没有对碰撞作出反应。
9. 创建一个可采集的脚本
现在你可以添加代码来处理碰撞了:
- 在项目窗口中,导航至 Assets > Scripts.
- 右击并且选择 Create > C# script。
- 命名新脚本为HealthCollectible。
- 在层级窗口中,选择你的康复包GameObject。把脚本从项目窗口拖放到属性窗口变成其组件。
- 双击脚本文件以在你的代码编辑器中打开。
- 删除Start和Upate函数,你并不想在游戏开始和每帧时做任何事。
- 你只想要脚本检测到Ruby碰撞康复包GameObject时能让它恢复一些体力。要做到这一点,使用下面这个新函数OnTriggerEnter2D:
void OnTriggerEnter2D(Collider2D other)
小贴士:确保你的函数名和参数类型不要拼写错误,因为Unity就是用它们来寻找该函数并回调的。
如同Unity每帧回调Update函数,它在检测到一个新的刚体进入触发器时的第一帧回调这个OnTriggerEnter2D函数。传入的参数是所进入的触发器的碰撞体。
- 添加一个简单的日志来检查是谁进入来触发器,代码如下:
public class HealthCollectible : MonoBehaviour
{
void OnTriggerEnter2D(Collider2D other)
{
Debug.Log("Object that entered the trigger : " + other);
}
}
- 回到Unity编辑器点击Play。现在当Ruby触碰到康复包时,控制台就会出现一个消息,告诉你Ruby进入了触发器。
10. Give Ruby Health
12. 调整RubyController的脚本
让我们检查下你的代码:
- 返回Unity编辑器
- 你会在控制台窗口发现一个错误:“RubyController.ChangeHealth(int)’ is inaccessible due to its protection level”。(RubyController的ChangeHealth(int)由于保护级别而不可访问。)
- 打开RubyController脚本。
- 还记得当你添加“Public”在maxHealth变量前边,从而让它出现在属性窗口中吗?对函数也是一样的。
- 如果不加任何作用域修饰符号,只可以在同一个脚本内访问你的函数。你可以在Update函数中调用ChangeHealth,但是HealthCollectible脚本不能访问这个函数。
要允许HealthCollectible访问它,只需要在其前方加上public
13. 检查Ruby是否需要恢复体力
14. 为RubyController定义一个属性
让我们开始为RubyController脚本定义一个属性:
- 打开RubyController脚本。
- 要在脚本中定义一个属性,请添加如下代码:
public class RubyController : MonoBehaviour
{
public float speed = 3.0f;
public int maxHealth = 5;
public int health { get { return currentHealth; }}
int currentHealth;
Rigidbody2D rigidbody2d;
// Start is called before the first frame update
void Start()
{
你已经像定义变量一样开始了属性定义:
- 访问级别(public)
- 类型(int)
- 名字(health)
不过这里我们不使用**;结束这一行,而是添加两层花括号代码块。
在第一层块中,你使用了get 关键字**,以获取第二层块内返回的对象。
第二层块就像一个普通函数,所以你只要返回currentHealth的值即可。
编译器的确把这个视作一个函数,所以你可以在get的函数体内写任意代码(比如申明变量,进行计算和调用其它函数)。
15. 在HealthCollectible中使用这个属性
你已经定义了一个属性,那么该如何使用呢?
让我们返回刚才的if语句:
- 打开HealthCollectible脚本。
- 在代码中找到if语句。
if(controller.health < controller.maxHealth)
属性用起来就像变量而不像函数。这里,health 会给你currentHealth。但它只在你读取它但时候工作。
如果你想要把它改为:
controller.health = 10;
Lesson8 世界交互 - 伤害区和敌人
在前一课中,你使用了一个触发器来检测Ruby是否触碰到血包。
本节课中,你会使用相同的知识在主角进入特定区域时受到伤害。
你还会增强敌人让它向前和向后移动,基于你学到的刚体组件相关知识,使用碰撞器而不是触发器。
1. 重置Ruby的血量
首先,我们先确保Ruby的血量不是上节课我们所改的1:
- 打开RubyController脚本
- 在Start函数初始化currentHealth为满血maxHealth。
现在开始本节课。
添加伤害区
添加一个可以伤害Ruby的区域。这有点像回血包,不过它会让血量减1并且触发后不会消失。
导航至 Assets > Art > Sprites > Environment 目录下,找到Damageable。
伤害区解决方案
现在我们检查一下这个方案:
- 用Damageable精灵来创建一个新的GameObject
- 添加Box Collider 2D组件,缩放Box到精灵大小,在属性窗口中选中Is Trigger。
- 创建Damageable GameObject对应的脚本,拖放到这个GameObject上。
- 双击编辑脚本,复制如下代码。这份代码和回血包的脚本几乎一样,除了它是减血的和去掉了Desory调用。
public class DamageZone : MonoBehaviour
{
void OnTriggerEnter2D(Collider2D other)
{
//通过collider对象尝试拿到Ruby的脚本组件,如果不为空则说明拿到了。
RubyController controller = other.GetComponent<RubyController >();
if (controller != null)
{
controller.ChangeHealth(-1);
}
}
}
相当不错,不过它只在主角进入区域时伤害主角。如果一直呆在伤害区里,它不会继续伤害。可以把OnTriggerEnter2D换成OnTriggerStay2D.这个函数每帧都会调用。
现在Ruby在伤害区会被持续伤害了,不过有点太快了!主角不到1秒就挂了,同时你还会发现主角尽管身处伤害区,但不移动的话也不会被伤害。
要修复后一个问题,你需要打开Ruby预制件,在Rigidbody组件里把Sleep Mode改为Never Sleep。
为了优化资源,物理系统在物体停止移动时不计算其碰撞体,这就是所谓的刚体的睡眠状态。但在我们的例子中,我们需要Ruby即使不移动也要计算其碰撞体。所以我们让其刚体永不睡眠。
接着我们修复Ruby“猝死”的问题。你可以让Ruby在一个很短的时间内处于“无敌”状体。该方式下,你可以忽略“无敌”态下受到的伤害。让我们对RubyController脚本做以下修改:
public class RubyController : MonoBehaviour
{
public float speed = 3.0f;
public int maxHealth = 5;
public float timeInvincible = 2.0f;
public int health { get { return currentHealth; }}
int currentHealth;
bool isInvincible;
float invincibleTimer;
Rigidbody2D rigidbody2d;
float horizontal;
float vertical;
// Start is called before the first frame update
void Start()
{
rigidbody2d = GetComponent<Rigidbody2D>();
currentHealth = maxHealth;
}
// Update is called once per frame
void Update()
{
horizontal = Input.GetAxis("Horizontal");
vertical = Input.GetAxis("Vertical");
if (isInvincible)
{
invincibleTimer -= Time.deltaTime;
if (invincibleTimer < 0)
isInvincible = false;
}
}
void FixedUpdate()
{
Vector2 position = rigidbody2d.position;
position.x = position.x + speed * horizontal * Time.deltaTime;
position.y = position.y + speed * vertical * Time.deltaTime;
rigidbody2d.MovePosition(position);
}
public void ChangeHealth(int amount)
{
if (amount < 0)
{
if (isInvincible)
return;
isInvincible = true;
invincibleTimer = timeInvincible;
}
currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
Debug.Log(currentHealth + "/" + maxHealth);
}
}
让我们深入理解脚本的修改细节!
- 添加三个新变量:
变量1:一个公共浮点型timeInvincible,每次无敌的总时间。公开这个变量可以使它出现编辑器中,用户就可以微调了。
变量2:一个私有布尔型isInvincible,表示当前主角是不是无敌状态。
变量3:一个私有浮点型invincibleTimer,表示主角剩余的无敌时间。随着时间流逝递减。 - 接着修改一些函数
ChangeHealth函数:在这个函数中首先检查主角是不是被可被伤害到,如果可以被伤害,再看是不是无敌状态,条件不满足就返回。
Update函数:在这个函数中如果主角无敌,就在剩余无敌时间中减去当前帧的delta时间。如果剩余无敌时间小于0,就结束无敌状态。
让我们进入播放模式测试下脚本,如果你让Ruby呆在伤害区,timeInvincible设置为2秒,Ruby会每隔2秒受伤一次。在属性窗口中你可以把这个值改为你想要的。
2. 图形批注
从代码中回来休息一下,我们看看精灵渲染器这个功能。现在,如果你想要通过用Rect tool(T键)缩放伤害区的方式生成一个大伤害区,精灵将会被拉伸而且看上去很丑:
不过你可以让精灵渲染器平铺精灵而不是拉伸它。这样以来如果你缩放伤害区到足够容纳精灵两倍大小,精灵渲染器会一个挨一个的绘制精灵两次。
视频
要实现这个:
- 首先,确保你的游戏对象的Transform Component(变换组件)中比例被设置为1,1,1。
- 其次,在精灵渲染器组件中改变Draw Mode绘制模式为Tiled瓦片,并且改变Tile Mode瓦片模式为Adaptive自适应。
这里会显示一个警告告诉你精灵可能显示错误。
你可以根据指示修正这个错误。在你的项目窗口中选择”Damageable Sprite“,并且修改Mesh Type为Full Rect。
点击属性窗口底部的Apply按钮,现在如果你在层级窗口中点击你的“Damage Zone”游戏对象,属性窗口将不再显示警告了。
现在当你使用Rect Tool 矩形工具缩放游戏对象时你会看到他拉升知道他可以容纳两个精灵,并且他显示两个精灵而不是完全拉伸,请注意,这仅仅在你使用瑞克工具的时候工作不是比例攻取,因为比例工具改变了优秀。
3. 敌人
在空寂的世界漫游了这么久,是时候添加一些其它角色了。让我们添加一些敌人,其实敌人也是一种伤害区。
保存下面的图片到你的计算机,导入Unity并放置到场景中。
就像你的主角,既然你想要敌人也能移动并和你的主角以及环境发生碰撞。它就也需要添加刚体和碰撞体。
别忘了设置重力比例为0,并且约束它不要绕z轴旋转。
视频
4. 前后移动敌人
创建一个新脚本命名为EnemyController,并且附加到你的敌人角色上。现在我们编码这个脚本来让敌人上下循环游走。
基于之前所学你想必已经胸有成竹,你可以试试自己先实现再来学习本文提供的解决方案。这里是一些重要的提示:
- 你需要获取刚体对象到你的脚本中的变量,使用MovePosition来移动它,记得在FixedUpdate中调用。
- 别忘了你可以暴露一个变量给编辑器以方便修改,例如速度。
下面是解决方案:
public class EnemyController2 : MonoBehaviour
{
public float speed;
Rigidbody2D rigidbody2D;
// Start is called before the first frame update
void Start()
{
rigidbody2D = GetComponent<Rigidbody2D>();
}
void FixedUpdate()
{
Vector2 position = rigidbody2D.position;
position.x = position.x + Time.deltaTime * speed;
rigidbody2D.MovePosition(position);
}
}
注意:tba
5. 伤害
6. 检查你的脚本
7. 小结
李大数小结
Lesson9 精灵动画
1. 动画器
2. 创建一个新的控制器
3. 动画
4. 改变一个精灵
5. 创建一个动画
6. 构建你的控制器
7. 使用混合树
8. 参数 Move X和Move Y
9. 发送参数到动画器的控制器
10. 设置主角的动画
11. 修改Ruby控制器的脚本
12. 检查你的脚本
13. 小结
李大数小结
Lesson10 世界交互 - 子弹
1. 创建子弹
现在我们来看看如何创建子弹:
- 要创建子弹,你要使用 Art > Sprites > VFX 目录下的CogBullet。
- 要让你的齿轮小一点,在层级窗口中选中它,在属性窗口中设置PPU为300并应用。你可以试试此值附近的值让它变大变小一点。
- 拖放精灵到层级窗口。
你需要给你的子弹刚体和碰撞体组件来检测碰撞。因为齿轮会移动并有可能碰到一些物体。
这次,缺省的Box Collider尺寸与精灵一样,因为你需要整个精灵都能碰撞。别忘了设置重力比例为0.
- 在脚本目录中创建一个新的脚本Projectile。
不同于主角由玩家移动的方式,导弹用物理系统。
通过给予刚体一个驱动力,物理系统基于这个驱动力移动刚体,你无需在Update函数中手动修改它的位置。
2. 物理系统
现在我们开始学习如何创建自己的物理系统:
- 首先,在脚本中定义一个Rigidbody2D变量,然后在Start函数中获取组件并赋值给变量。
Rigidbody2D rigidbody2d;
void Start()
{
rigidbody2d = GetComponent<Rigidbody2D>();
}
- 创建一个发射函数。以一个Vector2为方向,一个浮点数为力度。你用它们来移动刚体,力度越大,它移动得越快。
这个函数在刚体上调用AddForce,参数force是方向与力度的乘积,仍然是一个二维矢量。物理引擎将基于这个力度和方向来移动子弹。
public void Launch(Vector2 direction, float force)
{
rigidbody2d.AddForce(direction * force);
}
- 因为你想要检测碰撞,你会需要一个OnCollisionEnter函数。
这里,仅仅销毁子弹对象即可,稍后我们以此为基础来修复发疯的机器人:
void OnCollisionEnter2D(Collision2D other)
{
//we also add a debug log to know what the projectile touch
Debug.Log("Projectile Collision with " + other.gameObject);
Destroy(gameObject);
}
- 添加脚本到你的子弹GameObject,用这个子弹GameObject生成预制件。然后你可以删除场景中的GameObject,因为你并不想场景中有一个缺省的子弹。
3. 发射子弹
现在你已经有个子弹的预制件,你需要把它扔出去来修好机器人:
-
在RubyController脚本中添加一个公共变量,命名为projectilePrefab。你需要使其为GameObject类型;GameObjects像资源一样保存。因为你已经把它public了,它会以一个槽的形式出现在你的编辑器中,你可以拖放任何游戏对象给它。
-
拖放子弹的预制件到槽中。记得如果你在场景中操作的话,它会覆盖到你的预制件(一个小蓝条在那个条目中),所以使用Overrides下拉在预制件中应用。
-
接着,在RubyController脚本中写一个发射函数,这个函数在你想要发射一棵子弹时调用(例如当键盘按键被按下时)。
void Launch()
{
GameObject projectileObject = Instantiate(projectilePrefab, rigidbody2d.position + Vector2.up * 0.5f, Quaternion.identity);
Projectile projectile = projectileObject.GetComponent<Projectile>();
projectile.Launch(lookDirection, 300);
animator.SetTrigger("Launch");
}
4. 什么是Instantiate(实例化)?
Instantiate是一个Unity函数。它以一个对象为首个参数来创建对象的拷贝,第二个参数是位置,第三个参数是旋转角度。
让我们看看实例化的步骤:
- 要拷贝的对象是你的预制件,你会把它放置在角色刚体的位置(稍偏上一些以让子弹接近Ruby的手而不是脚),旋转矩阵为四元单位矩阵。
四元数是可以用来表达旋转的数学算子,不过这里你只要知道单位阵就是不旋转的意思就好。
- 然后你从新子弹对象中获取到子弹脚本组件,调用它的Launch方法,传入方向和力度。方向为主角目光方向,力度为300。
力度值设的很高以便用牛顿单位来表示。对于你的游戏来说这是一个非常好的值,不过如果你想要玩得更激烈,你可以暴露一个公共浮点变量用作力度。
同时,你可以看到你的动画器已经设置了一个触发器,这会让你的动画器播放一个发射动画!
- 最后的部分是检测玩家何时按下了键,并在按下时发射。
添加如下代码到Ruby Controller脚本的Update函数尾部:
if(Input.GetKeyDown(KeyCode.C))
{
Launch();
}
5. 发射齿轮
要发射一个齿轮你需要:
-
使用之前你见过的Input类,不过这次你要使用GetKeyDown,该函数允许你测试一个指定的按键是否被按下,此处我们使用键“C”。它只在键盘上工作。(李大数注:Input.GetKeyDown这个函数虽然每帧都会调用,但只在检测到按键down的那一帧中返回True,之后即被重置为False,并在后续帧中返回False状态,直到按键Up后重新开始检测,所以是安全的。)
-
如果你想要它能跨设备工作,你可以通过一个轴名来使用Input.GetButtonDown,就像你在移动中所做的,在输入设置中定义按键。例如Axes > Fire1.
-
现在按下Play,然后按下C来发射一个齿轮。 此刻你要么看不见齿轮,要么看见齿轮出现后马上消失。
6. 修正错误行
你的控制台有两个错误行:
- 一个空应用异常
如果你双击空引用错误,它会自动打开你的子弹脚本于Rigidbody.AddForce这一行,它的意思是你的刚体变量为空,尽管我们在Start函数中获取了刚体对象。
这是因为Unity不会在你创建对象时调用Start函数,而是在下一帧时才调用。所以当你调用发射函数时,仅仅是创建了实例而没有调用Start函数。所以你的刚体仍然为空。要修正这个问题,我们使用Awake这个回调函数。
与Start回调函数不同,Awake在对象创建时立即被调用(李大数注:类似C++的构造函数)。所以刚体在调用发射前被正确的初始化好了。
- 一条日志告诉我们齿轮和角色发生了碰撞。
对于第二个问题:你的齿轮子弹碰到了Ruby。原因是你在Ruby上创建了子弹,导致它刚被创建,物理系统就告诉你子弹刚体和Ruby发生了碰撞。然后你在**OnCollistionEnter中调用了Destory,子弹立即被销毁了。
你可以试试在这种情况下不销毁子弹。但是你还是会面临碰撞时子弹停止移动的问题。
7. 层和碰撞
修复这个碰撞问题的正确方法是使用层。层使得你可以对GameObject进行分组以便整体过滤。你的目标是做一个角色层来放置你的Ruby游戏对象,一个子弹层来放置你的子弹。
然后你可以告诉你的物理系统,角色和子弹层不会碰撞,从而忽略所有这些层之间的对象的碰撞检测。
-
要检视游戏对象在哪个层,点击位于属性窗口的右上角的层下拉箭头。所有的对象开始时都在Default层(层号为0)。你的游戏可以有多达32个不同的层。
-
如果你点击它,一个弹出窗口会出现,上面有一些内建的预定义层。不过你需要创建自己的层,所以选择Add Layer,这会打开层管理器。
层0-7被Unity锁定,你无法修改。所以我们在Layer8输入Charactor,Layer9输入Projectile。 -
现在打开Ruby预制件,把它的层改为角色然后保存。同样修改Projectil预制件的层为Projectile。
-
然后打开 Edit > Project Settings > Physics 2D ,注意面板底部的层碰撞矩阵,看看哪些层之间可以碰撞。
在缺省情况下,所有层都勾选,所以所有层都与其它层碰撞,但你可以不勾选角色层和子弹层,这样这两个层上的物体就不会碰撞了。 -
现在你可以进入Play模式,你的齿轮不会再和Ruby碰撞了,但它仍然可以和其它物体比如箱子或是敌人发生碰撞。
8. 修理机器人
第一步是在Enemy脚本中写一个函数来修理机器人和处理机器人被修好后如何反应。眼下,你的子弹仅仅是在碰撞时销毁了自己。但我们的目标是用齿轮子弹修好发疯的机器人。
要修好你的机器人:
4. 添加一个布尔变量broken,初始化为True。(李大数注:表示这个机器人发疯了,会四处移动攻击主角。)
5. 在Update和FixedUpdate函数开始处,测试机器人是不是疯了,如果没有疯就直接返回。
当机器人修好了,你的机器人就不会四处移动了。
因此,在update函数中早点返回就可以让它不再移动了:
void Update()
{
//remember ! inverse the test, so if broken is true !broken will be false and return won’t be executed.
if(!broken)
{
return;
}
void FixedUpdate()
{
if(!broken)
{
return;
}
- 在结束的地方,添加用来修理机器人的函数。这个函数非常简单:
//Public because we want to call it from elsewhere like the projectile script
public void Fix()
{
broken = false;
rigidbody2D.simulated = false;
}
你在这里设置了broken为假,刚体的模拟属性为假。
这将把刚体从物理系统的模拟中移除,所以它不会再介入到碰撞计算中,并且修好的机器人也不再会阻挡子弹或者伤害主角。
4. 现在剩下的就是修改子弹脚本的OnCollisionEnter2D函数了。从子弹的碰撞对象中获取到EnemyController组件,如果获取成功,就表示你已经修好了疯机器人。
void OnCollisionEnter2D(Collision2D other)
{
EnemyController e = other.collider.GetComponent<EnemyController>();
if (e != null)
{
e.Fix();
}
Destroy(gameObject);
}
注意:你也移除了Debug.log ,因为已经不再需要。
现在子弹将修好机器人,它将不再移动,你可以朝它走并且不会被伤害了。
9. 清理
一个小问题,如果你让Ruby扔出一个齿轮而没有碰到任何东西。这个齿轮会在整个游戏运行时一直运动,甚至飞出屏幕。
在游戏进程中,这可能会导致性能问题,特别是突然你有500个齿轮在屏幕外面飘荡时。
要修复这个问题,你只需要简单地检查下齿轮和世界中心的距离,如果远到Ruby不可能在遇到它(对你的游戏来说是1000单元),那你就可以销毁齿轮了。
让我们添加这个代码到齿轮子弹脚本的Update函数中:
void Update()
{
if(transform.position.magnitude > 1000.0f)
{
Destroy(gameObject);
}
}
记住Position可以被看作从对象所处世界的中心发出的一个向量,magnitude是这个向量的长度,即也就是到中心的距离。
当然有其它的方式来处理,这依赖与游戏。举个例子,你可以获取主角和齿轮的距离(使用**Vector3.Distance(a,b)**这个函数来计算位置a和位置b的距离)。
或者你也可以使用一个定时器,当齿轮发射设置4秒的定时,你可以在Update函数中递减它,当它到0时销毁齿轮。
10. 可选:修理机器人的动画
这一步是可选的,它并没有为游戏添加功能,也和本节课谈到的子弹没有关系,但它让游戏更悦目。
你将为修好的机器人添加一个动画。如何使用动画器创建动画请参见之前课程。
- 为敌人创建一个新的动画剪辑,命名为RobotFixed,设置采样率为4.
- 在敌人动画器中,从你的混合树到新动画剪辑创建一个变换。别忘记禁用Has Exit Time。没有必要变换到其它状态,因为一旦修好了,机器人就保持修好状态。
- 创建一个Trigger类型的参数命名为Fixed,设置其为变换的条件。
此刻在你的EnemyController脚本中,在Fix函数中,添加一行:
animator.SetTrigger("Fixed");
进入Play模式,发射你的齿轮到机器人身上来修复它。它应该会幸福的舞蹈!
11. 检查你的脚本
此刻RubyController脚本内容如下:
public class RubyController : MonoBehaviour
{
public float speed = 3.0f;
public int maxHealth = 5;
public GameObject projectilePrefab;
public int health { get { return currentHealth; }}
int currentHealth;
public float timeInvincible = 2.0f;
bool isInvincible;
float invincibleTimer;
Rigidbody2D rigidbody2d;
float horizontal;
float vertical;
Animator animator;
Vector2 lookDirection = new Vector2(1,0);
// Start is called before the first frame update
void Start()
{
rigidbody2d = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
currentHealth = maxHealth;
}
// Update is called once per frame
void Update()
{
horizontal = Input.GetAxis("Horizontal");
vertical = Input.GetAxis("Vertical");
Vector2 move = new Vector2(horizontal, vertical);
if(!Mathf.Approximately(move.x, 0.0f) || !Mathf.Approximately(move.y, 0.0f))
{
lookDirection.Set(move.x, move.y);
lookDirection.Normalize();
}
animator.SetFloat("Look X", lookDirection.x);
animator.SetFloat("Look Y", lookDirection.y);
animator.SetFloat("Speed", move.magnitude);
if (isInvincible)
{
invincibleTimer -= Time.deltaTime;
if (invincibleTimer < 0)
isInvincible = false;
}
if(Input.GetKeyDown(KeyCode.C))
{
Launch();
}
}
void FixedUpdate()
{
Vector2 position = rigidbody2d.position;
position.x = position.x + speed * horizontal * Time.deltaTime;
position.y = position.y + speed * vertical * Time.deltaTime;
rigidbody2d.MovePosition(position);
}
public void ChangeHealth(int amount)
{
if (amount < 0)
{
if (isInvincible)
return;
isInvincible = true;
invincibleTimer = timeInvincible;
}
currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
Debug.Log(currentHealth + "/" + maxHealth);
}
void Launch()
{
GameObject projectileObject = Instantiate(projectilePrefab, rigidbody2d.position + Vector2.up * 0.5f, Quaternion.identity);
Projectile projectile = projectileObject.GetComponent<Projectile>();
projectile.Launch(lookDirection, 300);
animator.SetTrigger("Launch");
}
}
你的EnemyController脚本内容:
public class EnemyController : MonoBehaviour
{
public float speed;
public bool vertical;
public float changeTime = 3.0f;
Rigidbody2D rigidbody2D;
float timer;
int direction = 1;
bool broken = true;
Animator animator;
// Start is called before the first frame update
void Start()
{
rigidbody2D = GetComponent<Rigidbody2D>();
timer = changeTime;
animator = GetComponent<Animator>();
}
void Update()
{
//remember ! inverse the test, so if broken is true !broken will be false and return won’t be executed.
if(!broken)
{
return;
}
timer -= Time.deltaTime;
if (timer < 0)
{
direction = -direction;
timer = changeTime;
}
}
void FixedUpdate()
{
//remember ! inverse the test, so if broken is true !broken will be false and return won’t be executed.
if(!broken)
{
return;
}
Vector2 position = rigidbody2D.position;
if (vertical)
{
position.y = position.y + Time.deltaTime * speed * direction;
animator.SetFloat("Move X", 0);
animator.SetFloat("Move Y", direction);
}
else
{
position.x = position.x + Time.deltaTime * speed * direction;
animator.SetFloat("Move X", direction);
animator.SetFloat("Move Y", 0);
}
rigidbody2D.MovePosition(position);
}
void OnCollisionEnter2D(Collision2D other)
{
RubyController player = other.gameObject.GetComponent<RubyController >();
if (player != null)
{
player.ChangeHealth(-1);
}
}
//Public because we want to call it from elsewhere like the projectile script
public void Fix()
{
broken = false;
rigidbody2D.simulated = false;
//optional if you added the fixed animation
animator.SetTrigger("Fixed");
}
}
你的子弹脚本内容为:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Projectile : MonoBehaviour
{
Rigidbody2D rigidbody2d;
void Awake()
{
rigidbody2d = GetComponent<Rigidbody2D>();
}
public void Launch(Vector2 direction, float force)
{
rigidbody2d.AddForce(direction * force);
}
void Update()
{
if(transform.position.magnitude > 1000.0f)
{
Destroy(gameObject);
}
}
void OnCollisionEnter2D(Collision2D other)
{
EnemyController e = other.collider.GetComponent<EnemyController>();
if (e != null)
{
e.Fix();
}
Destroy(gameObject);
}
}
12. 小结
本节课深入讲述了物理系统以及层的使用和力如何作用于刚体来让物体移动。
在下节课中,你将通过让摄像机跟随主角来把世界变得更大。
李大数小结
本节课要掌握以下三个要点:
tba
Lesson11 摄像机 - 电影模式摄像机(李大数注:后文简称电影机)
目前为止,你的游戏已经在一个屏幕上运行起来了,所以你已经使用了一个静态摄像机。
本节课中,你将使用一个名为Cinemachine的Unity包来自动控制你的摄像机而无需任何代码。你想让摄像机跟随你的主角以让它探索更大的世界。
注意:这节课需要网络连接,因为你将会下载Cinemachine包。
1. Packages(李大数注:这里当理解为类库包)
Unity附带了大部分游戏和应用都会用到的很多内置功能。更多不是所有游戏都会用到的特殊功能以包的形式提供。包仅仅增加了那些用到它们的项目的大小,并且很快捷和易于更新。
一个包是代码和资源的打包集合,你可以通过包管理器把它添加到你的项目。它们为你的项目添加功能让你无需编码,例如虚拟现实支持,后处理效果或者此处你正在寻找的:附加摄像机功能。
要添加Cinemachine包:
-
在Unity编辑器中打开包管理器(菜单:Windows > Package Manager).
-
查找Cinemachine条目,点击右下角的Install按钮。
-
Install按钮会变成Installing,当包完成导入时,它会变成Up to date。你现在拥有了Cinemachine,这是一个用来在你的工程里设置和移动摄像机的工具。
小贴士:点击View documentation版本号下方的蓝色链接可以查看包的相关文档。
2. 电影模式的设置
电影模式让你可以创建复杂的3D摄像机设置,允许在多个相机之间移动和剪切。
你即将用它来简单地创建一个2D方式下跟随目标的摄像机。
电影模式也包含2D辅助功能来约束摄像机到一定的边界,所以它不会在你的地图边界之外显示对象。
- 要开始使用它,你需要添加一个Cinemachine 2D Camera到你的场景,选择
3. 摄像机模式
4. 跟随主角
5. 摄像机边界
6. 把你的角色带回场景
要让你的角色返回场景,你将使用Layers,就像你在上节课对子弹所做的。让我们简单回忆下步骤:
- 在属性窗口的右上角,点击Layer下拉框并选择Edit Layer。
- 选择一个空的槽,命名为Confiner。
- 在你的Confiner GameObject,设置Confiner下方的层。
- 选择 Edit > Project Settings > Physics 2D,不勾选Confiner层的所有条目:
现在Confiner层的所有对象不会和任何对象发生碰撞。进入Play模式,四处移动Ruby。摄像机现在将停在地图边界。
7. 小结
本节课你已经导入了Cinemachine Package到Unity,并且使用它处理了摄像机的移动。
要获取使用Cinemachine的更多信息,看看文档,特别是你想用虚拟摄像机的属性来播放的时候,例如阻尼运动和跟随,以及跟随速度。
小贴士:关注下包管理器,因为它包含了大量可以帮你创建游戏,减小工作量的工具包。
下节课你会学到一些图形增强技术,通过使用粒子系统。
李大数小结
Lesson12 视觉设计 - 粒子
粒子被广泛地应用于交互式应用效果。一个粒子系统创建数十甚至上百的粒子。所谓粒子,就是有方向、速度、生命期、颜色和许多其它属性的小图片。通过播放这些参数,粒子可以聚合创建出像烟雾,火花甚至火焰这样的效果。
1. 准备精灵
Unity提供了一套广泛的粒子系统。本节课你可以用它为我们的游戏来创建一些粒子,例如疯狂机器人的烟雾效果。
烟雾效果
你将会使用精灵作为你的粒子群的影像。用于这些影像的精灵图集位于 Art > Sprites > VFX目录下,名为ParticleSheet。
- 设置类型为多个,PPU为100。
- 打开精灵编辑器切分不同的精灵。
- 精灵图集被切分为4行4列。如何切分请参考前课“精灵动画”。
当你有个所有精灵,就可以创建一个效果了。我们以烟雾效果开始。
2. 烟雾效果
要创建一个新的粒子效果:
- 使用层级窗口顶部的Create按钮,创建 Effects > ParticleSystem。
- 一个缺省的粒子系统已经为你创建好了。看起来像是很多白点向上飘动,把它重命名为烟雾效果。
- 在属性窗口中,你可以看见粒子系统由多段组成,每段都可以折叠,定义了系统和所有它创建段粒子的属性。
- 让我们开始把白点改为烟雾效果吧。在Texture Sheet Animation(纹理表动画)段中,点击临近的小圈来启用它。通过点击它的名字来打开这个段。
视频
3. 选择随机精灵
通常这个段中的设置用来随着时间流逝而动画化粒子图片。不过这里你只是用它来为每个粒子选择一个随机精灵。
- 设置Mode为Sprites。
- 点击精灵条目旁边出现的 “+按钮” 以得到2个精灵槽。
- 把两个精灵槽的属性赋为你的精灵图集中的烟雾效果。
- 点击Start Frame右侧的小下拉箭头,选择Random Between Two Constants,输入 0 和 2。粒子系统会挑选一个[0,2)之间的随机数(注意不包括2),所以只能是0或者1,并且为粒子使用相应的精灵。
- 最终,点击Frame over time旁边的黑色条,此处显示了帧如何随时间流逝而变化(从帧0到1)在一条曲线帧中。这里你不想要任何动画,所以右击最右边的点选择Delete Key:
视频
现在你可以在场景视图中看到粒子群了,tba
下一步是去调整它们如何被创建,因为此刻它们扩散的太远,扩散方向也太多。
-
在属性窗口中打开Shape section,场景视图将会显示粒子群发射的锥形。设置半径为0,因为你想要所有粒子从一个点开始(Unity中这个值会变成0.0001,不过不用担心,这只是因为该值不能为0,所以Unity把它设置为最接近的值)。
-
改变粒子扩散角度在5左右 tba
你的粒子群现在会以正确的方向开始,不过它们还是移动得太快了。另外由于所有的粒子形状相同,它们看起来很像人工产物。正常看到的形状是杂乱无章的,所以让一些东西看起来更自然的秘诀就是给它添加一些随机性。
4. 添加随机性给粒子群
在粒子系统顶部的主要段中,找到这三个设置:
- Start Lifetime:一个粒子的生命期是指它被粒子系统销毁之前在屏幕上生存多久。如果你向场景视图拉远镜头(拉远即缩小,更少细节然而可视范围变大,与之相反的是推近,更多细节然而可视范围变小),你会看到你的整个粒子群消失在相同的位置。因为粒子群的粒子都以相同的速度和相同的生命期开始,它们也在相同的距离后终止并销毁。
点击小下拉箭头tba
2. Start Size:这是粒子创建时的大小。此刻被设置为一个单精度浮点,故而所有粒子都是相同大小。我们通过选择Random Between Two Constants引入一点随机性,设置为0.3到0.5。粒子现在以各种不同尺寸变小了,不过还是移动得太快。
3. Start Speed:如法炮制,设置为0.5到1。
粒子群开始看上去更像烟雾了,不过还是有点奇怪,因为当粒子们到达生命期终点时,它们仅仅升起并突兀地消失。
视频
5. 让粒子淡出式消失
简而言之,我们要优雅地改变粒子的透明度,直到粒子的生命期终点变成完全透明。
在这种方式下,粒子不会突兀地消失,而是慢慢地淡出消失。
- 启用Color over Lifetime段,通过点击这个名字旁边的小白圈。
- 为了让所有的段更容易被看到,记住你可以通过点击它们的标题来打开段。点击颜色旁边的白色方块来打开Gradient Editor(渐变编辑器):
这个渐变展示来粒子的颜色在整个生命期内如何变化。底部箭头是颜色,顶部箭头是透明度(在游戏艺术中也叫做alpha)。
你会看到左侧的颜色和alpha tba
3. 现在,粒子会整个保持白色并且alpha不会改变。所以我们对它进行修改:选择顶部右方箭头,把alpha从255改为0。
视频
快到终点了!你的烟雾现在看上去好多了,不过还遗漏了一个小细节:烟雾应该随着时间流逝而逐渐熄灭,所以粒子的大小在生命期也应该改变,逐渐地变小。
就像对颜色所做的,有一个段叫做Size Over Lifetime,所以启用并打开那个段。
-
点击Size矩形,查看属性窗口底部出现的曲线:
此刻粒子系统所做的与你想要的恰好相反,因为粒子的尺寸在生命期内从小到大(注意这只是一个系数,所以它不会破坏我们之前设置的随机初始尺寸)。 -
让我们把它反过来,通过拖拽第一个点到1,最后一个点降低为0。
注意:你可以使用出现在每个点旁的正切来控制曲线,在场景中播放直到达到你满意的效果为止。对于一个好看的烟雾,你需要让曲线前半程平坦然后来个陡峭的降低。
视频
到这里你的烟雾看起来已经非常完美了,是时候把它添加到你的机器人上了。
6. 代码中使用粒子系统
让我们看看代码中的粒子系统
-
制作一个来自你的烟雾效果的预制件。然后你就可以删除场景中的那一个了。
-
打开你的机器人预制件,然后拖动你的烟雾预制件为你的机器人预制件的子元素,置于合适的位置让它看起来像是从头部发出的:
视频 -
保存你的预制件,试玩一下。机器人现在看上去更疯狂了。不过出现了两个问题:
- 烟雾会随着角色移动。这可不是我们所预期的烟雾工作方式-粒子不应该跟随机器人移动
- 如果你让Ruby扔出一个齿轮修好机器人,机器人仍然会冒烟。你需要在机器人修好后禁用烟雾粒子效果。
7. 修复烟雾的移动
烟雾移动的修复是非常简单的:
- 如果你查看粒子系统的主要段,你会看到一个Simulation Space(模拟空间)设置为Local(局部)。把它改为World(世界)。
同时在机器人修好后停止烟雾: - 打开EnemyController脚本,添加一个粒子系统类型的公共成员,命名为smokeEffect。
- 在机器人预制件的属性窗口中,出现一个新的槽。拖拽你的烟雾效果到这个槽。
你可能想直到为何类型是粒子系统而非GameObject,尽管你正把一个GameObject赋值给它。
这是因为如果一个公共成员是一个组件或者脚本(而不是GameObject),当你把一个GameObject在属性窗口中赋值给它时,Unity存储的是该GameObject拥有的该类型的组件。
这使得你不必像之前一样调用GetComponent。这也防止你赋值一个不拥有此组件的游戏对象到属性窗口设置,进而避免bug产生。
视频
4. 最后,在EnemyController的Fix函数中添加如下代码:
smokeEffect.Stop();
你的烟雾将会在修好机器人后停止。
此处有个问题要注意,为何这里要使用Stop,而不是像之前对子弹所做的一样简单销毁粒子系统?
实践出真知:
- 注释掉smokeEffect.Stop() ,在下一行添加Destory(smokeEffect.gameObject)。
- 播放场景,修好机器人。
你可以看到烟雾立刻消失了。这是因为当粒子系统被销毁时,它会销毁所有它当前处理中的粒子,甚至包括那些它刚刚创建的粒子。
Stop,与之相反,只是简单地停止粒子系统继续创建新的粒子,已经创建的粒子在其生命期到达前依旧存在。这就比一下子看到所有粒子全消失自然多了。
8. 创新时间
既然你知道了粒子系统是如何工作的,你可以尝试为你的游戏创建更多的粒子。本节课开始提到的精灵表里有一个样例粒子效果可以用于撞击和捡起医药包。
别担心做的不对,大胆创造和尝试各种不同的设置,在实践和错误中学会它的整个工作方式。
这里有一些信息可以帮你做到你期望的:
循环系统
你之前做的烟雾效果是无限循环的,所以它一直在产生新粒子。不过你也可以创建出只工作一定时间就销毁的粒子系统。要做到这个,导航至属性窗口中的Particle System section:
- 不勾选Looping。
- 设置Duration为你想要的效果持续时间。
- 设置Stop Action在这个段的底部来销毁。粒子系统将只在所有粒子消亡后销毁,所以它不会存在之前那个我们手动销毁循环粒子系统时“所有东东一次性消失”的问题。
爆炸式发射
你的烟雾效果以一个稳定的速率发射粒子。你可以在Emission段来设置这个速率。
Rate over Time 控制每秒发射多少粒子。tba
9. 检查你的脚本
10. 小结
李大数小结
Lesson13 视觉设计 - UI - Head Up Display
课程至此,你已经学会了使用Debug.Log在你的主角受伤时输出你当前的生命值。但是成品游戏的玩家不会有一个控制台窗口来查看日志。所以为了给他们反馈,交互式应用使用用户界面(简称UI),它覆盖了图片和文本在你的屏幕上来显示信息。这也被称为抬头显示(HUD)。(李大数注:HUD,抬头显示器,本是汽车上的一种设备概念,被用在了这里,也是颇为有趣。)
1. UI Canvas
本节课你将学习添加UI到你的项目来显示主角的血量。
Unity的UI用一种叫做Canvas的组件GameObject来渲染诸如图片,滑动条、按钮之类的控件。一个画板定义了每个UI元素如何渲染到屏幕上,并且渲染其所有的子UI元素。
要创建一个UI:
-
第一步是创建画板。在层级窗口中,右击选择UI > Canvas。
当你完成这步时,你会发现另一个GameObject也被加入了场景,它叫做事件系统。这也是一个游戏对象,它处理和UI的交互事件,例如鼠标点击。这里暂时还用不到它,但得留着它,否则画板会报警告日志。 -
选择新创建的画板,检查属性窗口。
2. Rect Transform(矩形变换)
第一处不同是这个GameObject有一个矩形变换组件取代了普通GameObject中的变换。
一个变换矩形仍然是一个变换的子类,所以脚本中可以把它像变换一样使用,不过你过会儿会看到它有一些增加的UI数据。此刻,我们先把它看做变换。
画板定义了UI如何显示在游戏中。画板可以是以下模式:
- 屏幕空间 - 覆盖:这个是画板的缺省模式,Unity任何时候都会绘制你的UI在游戏的顶部。大量应用使用这种模式,因为他们要用UI来显示信息。
- 屏幕空间 - 相机:这种模式绘制UI在对齐到我们相机的一个平面上。这个平面的大小可变所以总是填满屏幕,所以你可以任意移动相机而平面会随着相机移动如同覆盖一样。尽管如此,由于该平面并非绘制在游戏世界的上方,世界中的对象也可以出现在UI的上方。
- 世界空间:这种模式绘制的平面可以在世界的任意位置。举个例子,你可以用这个平面作为一个你的游戏中的计算机的屏幕,亦或是一面墙。这在3D游戏中非常有用,因为UI会随着距离变小。
在本节的例子中,你要保持画板模式为缺省值,即屏幕空间覆盖模式,因为你只是想显示Ruby的生命值。
3. 画板比例
画板有一个Canvas Scaler组件,定义了UI在不同的屏幕尺寸下进行比例变换。玩家可能在800x600,1920x1080等不同等分辨率下运行游戏。此外对于移动应用,app可能横屏也可能竖屏。所有这些情况都需要不同的屏幕尺寸和纵横比。
你可以设置Canvas Scaler模式为:
- Constant Size(可以是像素或者物理尺寸):这使得UI总是相同的大小,无论屏幕的形状和大小是怎样。这也能保证UI在任何屏幕上的可读性,但是过小的屏幕可能被UI占据大部分空间,导致重要的游戏世界元素被遮挡。
- Scale With Screen Size:这使得UI比例依赖于你设定的屏幕参考分辨率尺寸。
例如,如果你设置参考分辨率为800x600,而你的屏幕为1600像素宽,UI会放大两倍,也就是说,无论屏幕尺寸怎么变,UI总是占据相同的部分。
这种方式的坏处是UI可能会变得太小而难以阅读(如果你的基础尺寸大而实际屏幕小),或者UI变得模糊或者像素化(如果你的基础尺寸小而实际屏幕大)。
对本节课而言,我们使用Constant Pixel Size,因为你的UI最小能很容易去学习它。 - 最后我们要介绍的组件是Graphic Raycaster。你在本节课中不会用到它,但它让我们检测到按钮点击的事件。
4. 添加图片到UI
现在画板已经就位,你可以添加血条到UI了。它由左右两部分UI组成:
第一部分:左边是Ruby肖像的头像背景图。
第二部分:右边是一个计量血量的槽。蓝色部分会随着角色的生命值而变化。
(李大数注:这两部分是从视觉元素出发得到的概念,并非UI元素组成的层次说明,不要被误导)
要添加一幅图片:
- 右击画板,添加UI>Image子对象。
如果你打开游戏视图,你会注意到一个白色方形居于屏幕正中。这是因为你还没有为其赋予一个精灵对象,所以此刻它仅仅绘制一个白色方形。
但是如果你查看场景视图,白色方形就不见了。这是因为场景视图和游戏视图处理UI位置和大小的方式有一些不同。
到此为止,每个物体都以“单元”为度量,所以沿一条轴方向移动一个10单元表示世界中的10个单位,但我们的UI元素的矩形变换在这里以像素为单位。
所以如果你的屏幕是800像素宽,你的图片位置是400,它就会出现在屏幕的中间,但在场景视图中它会出现在400单元处,太远(也太大)导致我们看不到。
5. 在编辑器中编辑UI
- 要在编辑器中编辑UI,在层级窗口中选中画板,然后:
- 双击它或者
- 在你鼠标悬停在场景视图时按下F键
-
现在你可以看到白色方形和画板四个角上的蓝点了。并且在左下角你可以看到非常小的游戏世界。
-
让我们把白色块儿改为我们想要的图片,拖动Art>Sprites>UI中的名为UIHealthFrame的精灵血条游戏对象的Source Image属性中。
视频
图片被挤压得满满当当,这是因为它保持矩形的大小。 -
要通过改变图片的矩形变换来改变图片的大小,请点击图片属性中的Set Native Size按钮。现在你的图片应该变得巨大无比。
这是因为你的图片是1336像素宽,但你但屏幕比这小,不过你可以缩放它到合适的大小。
6. 调整图片大小
-
选择你的图片,确保工具栏中的Rect Tool被选中(快捷键:T)。
-
现在拖动角或者边来调整图片大小。如果你按住shift键,则会保持纵横比缩放。
-
你也可以点击并拖拽图片进行移动,它会紧贴画板的边角。让我们把它放置在画板左上角。
视频
你总是可以打开游戏视图来检查你的游戏元素的外观。放置图片只不过是第一步。
这时候,如果你改变屏幕大小(例如,改变游戏视图的大小),元素的放置状态会发生变化:
视频
7. 什么是锚点?
你的图片被锚定在屏幕的中心,但什么是锚点?看看你的场景视图中选中的图片:
屏幕的中心就是你的图片的锚点。那也是计算你的对象位置的起点(与之相联系的是图片的支点)
所以当屏幕缩放时,图片位置是不变的,图片不会和屏幕边界一起移动。
为了处理这种情况,你需要锚定图片到屏幕角上。当屏幕缩放导致角落移动时,你的图片也会移动相同的距离。
要锚定图片到角落:
-
你可以移动矩形变换的锚点,不过矩形变换组件能让我们直接定位锚点到某个角上。
-
点击矩形变换左上角的方块并且选择左上选项。
你可以看到在场景视图中锚点已经移动到左上角。
请注意此时矩形变换中的Pos X和Pos Y也同时发生了变化,这是因为锚点的改变导致图片坐标重新计算。
确保你的图片在角上,这样在你缩放屏幕时,UI始终在角上!
你可以重命名这个游戏对象为Health,这样我们就可以知道它是血条UI。使用属性窗口顶端包含Image的文本框,把它改成Health。
视频
8. 添加头像
到了添加Ruby头像和蓝色生命条来完成我们的UI的时候了。
要添加头像:
-
在Art>Sprite>UI目录下找到名为CharacterPortrait的头像图片
-
给Health Image创建一个新的子Image,把头像赋给它,点击Set Native Size后缩放大小(记得按住shift键以保持纵横比而不要挤压它)。缩放并且移动它到Health Bar背景的蓝圈处,直到你满意这个结果。
你无需在此改变锚点,我们这个case中就让它锚定在父对象的中心。这样当我们的血条因为屏幕缩放而移动时,它的中心也会移动,从而头像也会移动。
不过如果你对Health bar水平缩放时,它会改变中点从而改变头像位置,头像也会被缩放。
视频
这是因为锚点是一个参考点,它同时影响了位置和大小。如果你在Rect Transform矩形变换组件中按下anchor button锚点按钮,并且选择右下方的扩展蓝色箭头,你会看到4个箭头移到父图片上。
现在你的图片大小已经不是绝对大小而是相对于这些锚点的距离比例了。所以如果你的图片尺寸是左侧锚点到右侧锚点距离的25%,当这个锚点接近时,你的图片仍然会保持相同比例。
视频
3. 为了合适的缩放血槽,要把你的矩形变换属性改为拉伸式并且移动锚点。你可以点击并且拖放到围着你头像的合适位置。
视频
9. 蒙板方式遮盖血条
现在你需要添加蓝色血量条和想办法在主角受伤时减少血量条了。
你可以简单地设置血量条的比例来实现,然而这会导致血量条的挤压变形。
视频
取代方案是使用蒙板。蒙板是一种UI系统的技巧,让你可以把一幅图片作为另一幅图片的“蒙板”来使用。把你的第一幅图看作一个模板,第二幅图重叠于第一幅图的部分可见,其余则不可见。
10. 如何创建一个血量条蒙板
要创建一个血量条蒙板:
- 首先为血条GameObject创建一个孩子Image GameObject,命名为Mask。
- 缩放Mask,把它的锚点移到适合血条能达到的空白区。
视频 - 移动支点(蓝色空心小圆)到左侧边。之所以做这个,是因为我们用代码缩放时,将会从支点开始。
注意:如果你把支点留在中间,如果缩放20%,结果会是支点两侧各10%。这显然不是我们期望的。通过把支点放在左侧边,你可以确保仅仅在右侧缩放20%。 (李大数注:这里要移动支点,需要把Unity顶部工具栏上的Center/Pivot二态按钮设为Pivot状态,否则是无法拖动蓝色空心小圆的。)
视频
你不用赋Sprite给它,因为你只是想把它当蒙板用,而缺省的白色矩形已经可以完美满足需要。 - 创建Mask的子图片对象,赋予其UIHealthBar蓝色血量条图片,点击其矩形变换中的锚点图标。
- 现在,按住Alt键,点击右下的拉伸锚点图标。这能同时设置锚点和新图片的尺寸以适合父窗体,故而你的血量条会自动设置正确的大小。完成后,再次打开此图标,此时不要按住Alt键,仅仅设置血量条的锚点到左上角。
- 再次点击Mask,选择添加组件,搜索Mask并添加它。取消勾选Show Mask Graphic来隐藏白色方块。
- 缩放Mask,小心点这里不要选择血量条,要选择Mask。
视频
这里隐藏了血条。因为矩形是一个蒙板。当你减小它时,下面未重叠部分的血条被隐藏。这也是为何你要设置血条锚点到角上而不是随着父对象变换的原因,如果血条随着musk做比例变换就违背了我们的初衷。
11. 血量条脚本编程
血量条的可视部分已经完成,让我们看看脚本如何处理。
- 创建一个新脚本命名为UIHealthBar。
public class UIHealthBar : MonoBehaviour
{
public Image mask;
float originalSize;
void Start()
{
originalSize = mask.rectTransform.rect.width;
}
public void SetValue(float value)
{
mask.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, originalSize * value);
}
}
- 这里没什么新东西,除了以下两点:
- 用rect.Width获取屏幕大小
- 用SetSizeWithCurrentAnchors设置尺寸和锚点
-
当血量在0到1(1为满血,0.5为半血,以此类推)之间变化时 ,你的代码将调用SetValue。这会改变我们的Mask的尺寸,接着Mask会隐藏血条的右边部分。
-
现在如果回到Unity编辑器让你的脚本被编译一下,你会得到一个错误“The type or namespace name ‘Image’ could not be found”。这是因为Image不是UnityEngine的名字空间的一部分,而是其子分类UnityEngine.UI的一部分。
我们可以通过完整输入UnityEngine.UI.Image来修复这个问题,不过你可能会有疑问,GameObject是UnityEngine名字空间的一部分,为何并没有完整输入UnityEngine.GameObject就能使用它?
看看你的脚本的最上面几行,using关键字实现了把包名字空间的所有类型导入脚本。
比如这一行"using UnityEngine;"就导入了UnityEngine中的所有类型,这样就可以无需在类型前再键入UnityEngine了。同理添加“using UnityEngine.UI"后就可以直接使用Image,当然代码也可以顺利编译通过。
最后要做的是在主角血量改变时调整血条这个UI元素,你需要从RubyController脚本的ChangeHealth函数中调用SetValue来提供一个新的血条显示量。
12. 引用
让我们仔细看看这种通过使用一个静态成员来引用某个东东的新方法。之前,当你在脚本中创建一个成员时,每个实例中都会有一份拷贝。而静态成员是所有实例共享该成员变量。我们用类名.变量名来访问它。我们之前用过的Time.deltaTime也是一个静态成员变量。静态成员还可以是一个函数,例如我们用过的Debug.Log。
在这个case中,你想要在其它任何脚本中不通过UIHealthBar的引用来访问,那我们就要做如下修改:
public class UIHealthBar : MonoBehaviour
{
public static UIHealthBar instance { get; private set; }
public Image mask;
float originalSize;
void Awake()
{
instance = this;
}
void Start()
{
originalSize = mask.rectTransform.rect.width;
}
public void SetValue(float value)
{
mask.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, originalSize * value);
}
}
让我们深入看看有哪些改变:
-
static UIHealthBar instance property,血条实例这个属性是静态和公共的,所以你可以在任意脚本通过UIHealthBar.instance来调用其get属性。不过set属性仍然是私有的,因为我们不想从脚本外部修改它。
-
在你的Awake函数(记住此函数在对象创建时马上调用,在我们的例子里对象在游戏启动时就被创建了)中我们存储这个静态实例为this。this是一个特殊的C#关键字,用来表示当前运行的成员函数所操作的对象。
当游戏启动时,HealthBar脚本的Awake被调用,Awake函数中存放自身引用到其静态成员“instance”中。从而其它脚本调用UIHealthBar.instance时就会返回我们场景中的血条脚本。
你现在有一个场景中血条脚本的引用,无需再次在属性窗口中手动赋值了。为何不把所有对象都这么用呢?正如你之前看到的,静态成员被脚本的所有实例共享,也就是所有挂接了这个脚本的游戏对象中该类型的对象都是同个对象。
如果你的场景中有两个血条,第二个将也把自身存储在静态成员中,替换了第一个。导致UIHealthBar.instance总是返回第二个。这就是所谓的Singleton(单件)模式,因为该类型只有一个对象可以存在。这精确符合你的需求:只需要一个血条。
13. 更新血条
现在让我们在游戏中动态刷新血条。打开RubyController脚本,在ChangeHealth函数中,把Debug.Log这一行替换为:
UIHealthBar.instance.SetValue(currentHealth / (float)maxHealth);
你现在可以添加UIHealthBar脚本到你的血条游戏对象了,拖动你的蒙板到属性窗口中的Mask属性,进入游戏模式。如果Ruby被敌人或者伤害区伤害。其血条会相应刷新。
14. 检查你的脚本
你的RubyController脚本现在应该看起来像这样:(李大数注:此处代码省略,见原文链接)
你的UIHealthBar脚本现在应该看起来像这样:(李大数注:此处代码省略,见原文链接)
15. 小结
本节课你已经看到了Unity如何渲染一个UI,以及你如何在编辑器中使用Rect tool来放置和缩放元素以便它在各种比例下良好工作。
你也学到了如果在脚本中使用静态成员,以及单件模式,让你的UIHealthBar脚本可以被任意地点访问。
下节课你将添加一个你可以与之谈话的角色来增强你的UI,同时介绍一个用于视频游戏制作的重要概念:射线检测。
李大数小结
本节课核心要掌握三个要点:
第一是锚点的概念,这可以说是Unity的一个重大创新概念,Unity的锚点不但可以是一个点,还可以是一个矩形区域(四个锚点形成一个锚区),这样当父对象缩放时,既影响到了子对象的位置,也影响到了子对象的大小,子对象的大小会随着锚区的缩放按比例变换。
第二个要点就是“蒙板”的概念,这里的蒙板与我们直觉的蒙板概念恰恰相关,我们直觉的蒙板类似面具,指的是不透明材质组成的部分,蒙板覆盖的部分是不可见的,没被覆盖的部分是可见的(想象一下火影中卡卡西的面具)。而Unity的蒙板是被覆盖的部分是可见的,没被覆盖的部分是不可见的,它指的是由透明材质组成的部分,我们称其为“透板”可能更便于形象记忆。
第三个要点是“单件”,这是程序开发领域中设计模式相关的概念,称其为最重要的设计模式也不为过,通过单件,我们可以快速访问所有全局唯一的对象,比如音频管理器,资源管理器等等。
Lesson14 世界交互 - 对话框和射线检测
Ruby现在已经可以修好疯狂的机器人了,不过她仍然是这个孤独世界上唯一的居民。让我们添加另一个角色到场景中。为了让新角色有趣一点,我们让Ruby可以和它谈话,并且从它那里知道自己的“小目标”:修好所有的机器人!
1. 创建角色
第一个任务是创建角色,一只叫做Jambi的青蛙。现在你已经熟悉了Unity,那就让我们直截了当搞快点。为了让事情简单点,这个角色使用一个单循环动画。
要用到的精灵图集是位于Art>Sprites>Characters目录下的JambiSheet。
-
切图为4x4Cells
-
导入精灵来创建GameObject角色
小提示:如果你在你的项目目录中选择三个精灵,并且一次性拖放它们到层级窗口,Unity不但为创建新对象,同时自动为这个对象创建由这些精灵生成的动画。一切都是自动的,无需其他操作。(李大数:这个操作技巧非常方便和实用,建议大家一定要熟练掌握)
视频
-
回到项目窗口中你的Jambi精灵图集, 修改图片的PPU让它看起来更好一点,改变数值后记得要Apply一下,这个case中150比较合适。
-
添加一个Box Collider 2D组件,并且调整其比例使得它能覆盖角色的底部,就像我们之前对主角和敌人所做的一样。
-
创建一个名为“NPC”(李大数:Non Player Character,非玩家角色)层,把你的角色对象至于其上。
-
最后,重命名GameObject为Jambi,并且用它做一个预制件。
你现在可以进入游戏模式,测试下你的角色的动画和碰撞行为是否正确。
如果自动创建的动画看起来运行得太快,你可以在动画窗口(Menu Windows > Animation > Animation)中调整采样率。就像之前你处理动画剪辑一样。
2. Raycasting(射线检测)
现在你需要添加一些代码,以便Ruby可以和你的青蛙角色谈话。首先,你需要知道Ruby是否站在那个角色的前方。
你可以在那个角色的前方放置一个触发器,当Ruby经过触发器是,对话即开始。但这也意味着即使Ruby背朝青蛙时仍然可以与其对话。
因此,我们应当在交互式应用中使用一种物理系统相当有用的功能:射线检测。
Raycasting是在场景中投射一道射线以检查射线是否与碰撞体相交。一道射线由起点,方向和长度组成(可以想象为射线段)。“投射“一道射线这样的术语表示从起点到终点沿着射线方向进行相交的测试。
这里你将要投射的一道射线,应当从主角的位置开始,沿着主角目光方向,长度为1到1.5个单元。
为了实现这个,添加如下代码到你的 RubyController 的 Update function :
//李大数:检测“X”键是否被按下
if (Input.GetKeyDown(KeyCode.X))
{
//李大数:调用2D物理引擎的射线检测方法,传入起点、方向、长度、过滤层掩码,返回Hit对象
RaycastHit2D hit = Physics2D.Raycast(string point, dir,len,LayerMask.GetMask("NPC");
//李大数:如果Hit对象内的碰撞体为空,说明没有发生碰撞
if(hit.collider != null)
{
//李大数:不为空,说明碰撞到了NPC,在这里简单Log出碰到的对象的名字
Debug.Log(hit.collider.gameObject);
}
}
3. 对话框UI
处于简化目的,你将让对话框在青蛙朋友Jambi的上方出现。
你会使用一个Canvas,但这次我们将会让画板出现在世界空间中。也就是说画板在游戏世界中真实存在,就在Jambi的头顶,而不是永远盖在屏幕上。
要添加Canvas:
-
在层级窗口中右击Jambi(或者先选择Jambi然后点击层级窗口顶端的Create按钮)
-
选择UI > Canvas。此操作为Jambi创建一个子Canvas GameObject。(李大数注:注意场景视图上方工具栏的“Gizmos”为”按下“状态才能看见Canvas的边框)
此刻你的Canvas叠在屏幕上。选中Canvas,在属性窗口中把Render Mode改为World Space。
你可以忽略Event Camera设置,因为它只在类似按钮按下之类操作的UI交互中用到,此处你只是想在画板上显示文本而已。
刚刚生成的Canvas太大了。这个Canvas的尺寸是像素,我们可以在Rect Transform中看到它的Width和Height都是以百计(这个数值可能不同,取决于你在把画板切换到世界空间时你的游戏视图的大小)。
你可以改变这些数值以给予画板一个合适的大小(比如3x2),但这令你构建UI更麻烦了。
所有的UI元素(例如图片和文本)都以像素为单位,所以一个3x2的Canvas大小会是一个3x2像素的盒子。
故而,你应该对画板做比例变换,以便其保持合适的尺寸。
设置你的Rect Transform(矩形变换):
-
坐标x,y都为0
-
宽度300,高度200
-
比例x,y,z都为0.01
4. 改变你的场景
此刻你的画板已经在场景中有个合适的大小,让我们把它移到青蛙角色的上方。
添加一个图片:
-
选中青蛙的画板子对象。
-
右击GameObject,选择 UI > Image 。
-
在项目窗口中,导航至 Assets/Art/Sprites/UI目录。
-
选择UIDialogBox精灵,拖到GameObject的Source Image字段。
别忘记扩展图片以填满画板,在Rect Transform(矩形变换)中按下Alt键的同时选择bottom right handle。
视频
你可能注意到你的画板图片出现在你的场景中某些元素的后面。这是因为你的画板也存在于场景中,所以它像任何其它游戏对象一样可以在其它游戏对象之后渲染。
要确保画板在最顶端渲染,选择层次窗口中的画板,在属性窗口中,设置Order in Layer为一个较高的值(例如10):
5. 在画板上添加文本
要添加文本到画板,我们需要:
-
在层级窗口中右击Image这个GameObject,选择UI > Text-TextMeshPro。
-
点击Import TMP Essentials(导入项目必要的Text Mesh Pro 资产)。当导入完成时(Import TMP Essentials按钮会变为灰色不可用),你就可以关闭此窗口了。而你的文本此刻已经就位。
-
就像之前调整图片一样,通过按住Alt键同时streach方式扩展文本为整个图片的大小
-
然后用小白手柄来移动黄色的文本外框,给文本框一个边缘留白。
视频 -
在属性窗口中,写下文本,改文本风格,试玩一下各种参数。
中文显示(李大数补充)
由于Unity缺省只提供了西文字库(字库中只有数字和英文的字模图形),所以原文中的方法只适用于英文显示,如果在文本框中输入中文,会因为缺少中文字模而显示为空白格,所以在这里补充制作Text Mesh Pro所支持的中文字库(字库中包括数字、英文和中文)的方法。
- 找到自己喜欢的没有版权问题的中文字库,TTF格式为宜。
-
这个字库应该足够全,包括所有西文字符和中文简繁体字符。
-
这个字库应该是开源字库或者商业授权字库,不应该有版权问题。
-
这样的字库目前已经很多了,比如站酷,阿里,谷歌都有提供。
- 创建自己所用字的文本文件,TXT格式。
-
这个文件本质上是一个索引文件,以此文件的每一个字为索引,在真正的字库文件中对应到真正的字模。
-
这个文件中除了需要的中文,也应该包括所有数字和英文大小写字母以及标点符号。
-
在Assets目录下创建Fonts目录,导入上面两个文件到此目录。
-
选择 Window > TextMeshPro > Font Asset Creator,在弹出的字体资产创建窗口中:
- Source Font File 选择导入的TTF字库 。
- Chracter Set 选择 “Chracters from File”。
- Chracter File 选择导入的TXT文件。
-
在窗口中点击Generate Font Atlas,等待生成完成。
-
点击“Save”保存生成的字体资产文件。此时如果你在Finder中查看,会发现其扩展名为.asset。
-
点击场景中TMP文本游戏对象,在属性窗口中的“Font Atlas”条目中选择你创建好的字体资产。此时你就可以在场景视图中看到中文了。
6. 显示对话框
最后一步了,你要完成当Ruby和Jambi谈话时显示对话框。
对此,首先通过禁用来隐藏画板:
-
层级窗口中选择青蛙的子画板,属性窗口中顶部的复选框取消勾选。
-
为Jambi创建一个新的NonPlayerCharacter脚本。
-
在脚本中,定义两个公共变量:
-
一个浮点型变量,命名为displayTime,初始化为4,用来存放对话框显示了多长时间
-
一个游戏对象类型的变量,命名为dialogBox,用来存放画板游戏对象,以便启用/禁用它。
- 然后创建一个私有变量:
- 一个浮点型变量,命名为timerDisplay,存放对话框显示多长时间
public float displayTime = 4.0f;
public GameObject dialogBox;
float timerDisplay;
- 在Start函数中,我们需要确保“dialogBox” 被禁用,同时初始化timerDisplay为-1:
void Start()
{
dialogBox.SetActive(false);
timerDisplay = -1.0f;
}
- 在Update函数中,通过测试timerDisplay是否大于等于0来检查对话框是否在显示。
如果它大于0,则对话框在显示:这种情况下你应该不断减去Time.deltaTime来检查timerDisplay何时为0。此时就可以隐藏对话框了。
void Update()
{
if (timerDisplay >= 0)
{
timerDisplay -= Time.deltaTime;
if (timerDisplay < 0)
{
dialogBox.SetActive(false);
}
}
}
- 最后,你需要写一个公共函数DisplayDialog,你的RubyController会在和NPC青蛙交互时调用它。这个函数将显示一个对话框并设置timeDisplay为定义好的displayTime。
public void DisplayDialog()
{
timerDisplay = displayTime;
dialogBox.SetActive(true);
}
- 再次打开RubyController脚本,替换Deubg.log为如下射线检测代码:
if (hit.collider != null)
{
NonPlayerCharacter character = hit.collider.GetComponent<NonPlayerCharacter>();
if (character != null)
{
character.DisplayDialog();
}
}
-
到这里你所做的是检查是否有碰撞,如果有,那么看看射线检测方向是否可以获取一个NPC脚本组件对象,如果脚本获取成功,就显示对话框。
-
别忘了给青蛙对象NPC脚本组件中的Dialog Box条目分配青蛙的子画板对象。
-
现在再试试让Ruby面向Jambi时按下X键。这次,对话框会出现并在4秒后消失(或者以你在属性窗口中设置的displayTime值为秒数)。
7. 检查你的脚本
此时你的RubyController脚本、NonPlayerCharacter脚本应当如下:
(李大数:为节省篇幅,代码见原文)
8. 小结
这一节课中已经看到了射线检测在交互式应用中非常有用,因为他允许你在给定的方向上检测碰撞。
除了检测角色的前方有什么,射线检测根据你想制作的游戏类型还有很多用途。
举个例子,要检查主角和敌人之间是否有其它东西,你可以用这两个点之间的射线检测,如果返回一个hit对象,那么一定有一些物体在他们之间,所以敌人此时是看不见主角的。
李大数小结
本节核心是掌握三个要点。
-
第一个要点是“射线检测”的概念,用射线检测的Hit函数来测试命中哪些碰撞体,然后操作。
-
第二个要点是如何显示一个对话框
-
第三个要点是锚区的缩放如何影响子对象的大小
-
锚区用上下左右的规范化值来表示,当锚区收缩为一个点时就是“锚点”,此时上下相等,左右相等。
-
锚点状态时父对象的缩放不影响子对象的大小。
-
锚区状态时父对象的缩放会影响子对象的大小,此时子对象不再用(x,y,w,h)表示,而是用(l,r,t,b)来表示,这四个变量是和四条锚边的偏移像素值,向内为正,向外为负。此时子对象的位置不再由支点相对锚点的偏移给出,而是直接由四边给出。这种锚区方式用在主角的生命条上很方便。
-
Lesson15 音频
到目前为止,你的游戏一直是无声的。在本节课中,你会学到如何使用音频文件来播放一些背景音乐或者当Ruby拿到一个医疗包或者修好一个机器人的时候播放一个声音。
1. 音频剪辑,音源和音频听者
Unity声音系统由以下几部分组成:
-
音频剪辑
音频剪辑属于资源,如同纹理或者脚本。你可以从一个音频文件中导入音频剪辑,例如mp3,ogg和wav文件,它们存在于你的项目目录中。你可以在Audio目录下找到用于本节课的音频剪辑。 -
Audio LIstener(李大数注:从上下文来看,这里翻译为“音频听者”比较合适)
音频听者是一个组件,定义了“听者”位于场景的何处。这对于你想在左方/右方扬声器(或者某些装置中的后方/前方)播放更多的声音以便提供环绕玩家的声音时很有用。
缺省情况下,它位于摄像机上。因为玩家期望从相机位置听到声音,所以屏幕右侧的声音应该在右方扬声器播放。
如果你在层级图中点击摄像机,你可以看见它上面有一个Audio Listener组件。
- 音源
音源是一个组件,它让你在音频剪辑所属游戏对象的位置播放该音频剪辑。如果启用音频特效,它相对“音频听者”的位置可以让音频系统正确地混合声音。
即使你不使用特殊的声音,对于这个例子中的背景音乐,你仍可以使用居于游戏对象上的音频源,因为它可以播放声音。
2. 背景音乐
让我们开始制作一些可以播放的背景音乐:
- 创建一个空的游戏对象,命名为BackgroundMusic,添加一个音频源组件给它:
- 拖放项目中Audio目录下名为“2D MUSIC LOOP”的音频剪辑到AudioClip属性槽。
- 确保你勾选Loop选项,这样你的音乐才能循环持续播放。
- 检查Spatial Blend滑条,该滑条可以从2D(左侧)到3D(右侧)滑动。
这里定义了声音是否要特殊化。如果滑条全程2D,声音不会特殊处理,无论声音听者在哪里都以相同的级别播放。
这有一点像在播放器中听音乐,音乐被设置为立体声。如果滑条全程3D,声音会在左右扬声器或多或少调整,依赖于音频源相对音频听者的位置。
由于本例子中你需要无论哪里都听到一样的音乐,确保滑条全程2D。你现在可以忽略其它属性。
小提示:如果你很好奇,点击右上角有个问号标记的图书图标来打开Unity手册。
- 现在进入播放模式,你会听到音乐在游戏开始时马上开始。如果音量太大你可以调整音量滑动条。
注意:记得游戏运行时你做的任何改动都会在退出游戏模式时撤销,所以确保不要在播放模式下调整设置。 - 如果开发期间因故你需要静音,点击游戏视图右上角的Mute Audio按钮。如果应当有声音但你听不到任何声音,首先看看这个按钮是不是被启用了。
3. 单次声音(李大数注:通常称之为音效)
当你需要一直播放的声音时分配一个声音剪辑是非常好用的。不过有时你需要在某事件发生时只播放一次声音,例如角色得到可以补血的物品时。
一种操作是创建一个新音频源,分配补血包音频并且不勾选Play On Awake选项,让它不要在游戏开始时播放。
之后在脚本中你可以在事件发生时播放音频剪辑。不过这需要为游戏里的每一个小声音都创建游戏对象和音频源。
与之相反,你应该使用PlayOneShot这个音频源函数。与Play不同,后者播放分配给音频源的音频剪辑,PlayOneShot把音频剪辑作为第一个参数并且只播放一次,使用音频源的所有设置和位置。
所以你可以添加一个音频源到我们的Ruby主角,并且使用那个音频源来播放所有游戏中的主角动作音效,例如拿到血包,扔出齿轮或者被击中。
注意:如果你在场景中添加音频源到Ruby而不是预制件模式,记得在属性窗口中应用并覆盖。
4. 添加音频源到Ruby之后
-
打开RubyController脚本,添加私有AudioSource变量,命名为audioSource,在Start函数中调用GetComponent获取音源对象并设置给此变量。
-
接着我们写一个函数PlaySound,它以一个音频剪辑为参数,调用音频源对象的PlayOneShot函数。
AudioSource audioSource;
void Start()
{
rigidbody2d = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
currentHealth = maxHealth;
audioSource= GetComponent<AudioSource>();
}
public void PlaySound(AudioClip clip)
{
audioSource.PlayOneShot(clip);
}
- 现在,打开HealthCollectible脚本,添加一个音频剪辑类型的公共成员,命名为collectedClip,然后调用RubyController的PlaySound函数。
public class HealthCollectible : MonoBehaviour
{
public AudioClip collectedClip;
void OnTriggerEnter2D(Collider2D other)
{
RubyController controller = other.GetComponent<RubyController>();
if (controller != null)
{
if (controller.health < controller.maxHealth)
{
controller.ChangeHealth(1);
Destroy(gameObject);
controller.PlaySound(collectedClip);
}
}
}
}
为何使用Ruby对象上的Audio Source而非收集物品对象上的呢?😁,因为音频源是一个组件。当Ruby抓住收集物品时,收集物品对象被销毁,自然附着其上的音频源组件也被销毁,声音嘎然而止。
通过播放Ruby上的音频源,声音在收集物品销毁时就仍然能持续播放了。
-
现在检查你项目窗口中的血包预制件。一个新的名为Collected Clip的槽出现并可用,拖放Aduio 目录下名为Collectable的音频剪辑到槽中。
-
你现在可以进入播放模式,让Ruby抓取血包来听听音效 - 别忘了先让Ruby被机器人碰一下掉点血!
5. 练习
作为一个练习,你可以试着添加Ruby扔齿轮或者被敌人撞到的声音,你需要做的是:
- 添加一个公共的Audio
6. 特殊声音
7. 什么是最大距离?
8. 修正减弱
9. 检查你的脚本
10. 小结
李大数小结
Lesson16 编译、运行和发布
你已经深入学习了创建游戏所需要的每一步。你看过如何导入资源,写脚本,使用物理引擎,使用瓦片地图,创建粒子效果乃至添加音频。
既然你的游戏已经完成了,你将需要构建它来创建一个单独的应用,然后就可以上传到数字商店了。
接下来大家就能够玩这个游戏而无需安装Unity编辑器,同时你所有游戏的资源都在他们的设备中。
1. Player Settings(李大数注:这里的Player是指游戏应用本身)
你用编辑器创建好的要分发给用户的应用程序叫做Player。在创建Player之前,让我们快速看一下Player设置。
- 要找到