Unity上道秘籍

  • 本文仅用于理解Unity的基础代码运作逻辑
  • 能让人快速入门Unity及Unity的编程

🔥Unity的世界观

  • 本节主要介绍Unity界面与基础脚本通信
  • 提示:强烈建议使用英文编辑器!!

🚩界面介绍

  • 默认你有一点点的C#基础,因为我也只会一点点的C#基础👍
  • 下图是Unity新项目的界面
    • Unity引擎界面由许多窗口构成,窗口能够互相吸附、打开关闭
    • 两个窗口可以像网页标签一样重叠
    • 标签右方带锁头的窗口可以被上锁,可以固定显示某个物体的状态
    • 在标签栏里Window中可以调出所有的窗口,窗口都可以多开
    • 恢复及设置窗口布局点击界面右上角Layout

Scene场景窗口

  • Scene窗口用于显示一个场景文件Scene里的所有内容
  • 鼠标右键拖动:以自身为基准旋转(忽略X轴旋转)
  • Alt + 鼠标左键拖动:以视角中场景的中轴旋转(忽略X轴旋转)
  • 鼠标中键拖动:以固定比率移动自身
  • 鼠标中键点击:若点击至物体,视角会聚焦该物体,中键拖动所移动的比率会智能变化
  • 鼠标滚轮:推动视角纵深
  • 使用视角辅助器能将视角转换为正视角和正交视图

Game游戏窗口

  • Game窗口以列表的形式显示Scene场景摄像机组件内的视图
  • Game窗口左上角的Display序数必须与此时显示的摄像机相匹配
  • Display序数选项旁可以设置输出视角的分辨率
  • Maximize On Play全屏播放选项若开启,运行游戏时会使Game窗口最大化

Hierarchy大纲

  • Hierarchy窗口用于显示Scene场景中所包含的所有游戏物体
  • Hierarchy窗口中单击右键可以新建几乎所有的Unity预设物体
  • 拖动窗口中的物体至另外的物体上,可以设置父子集关系
  • 一般场景物体的图标和字体显示黑色,预制体的图标和字体显示蓝色
  • Hierarchy窗口中预制体的右边显示箭头,可以将场景转换为预制体编辑模式
  • 复制物体使用ctrl + D,删除物体使用Delete
  • 多选物体时,按ctrl键加选减选
  • 连选物体时,先选择首物体,按shift键选择尾物体

Project项目文件

  • Project窗口以列表的形式显示整个项目的所有资源文件
  • 推荐使用文件夹分类的方式进行文件管理(右键->Create->Folder)
  • 默认的Project窗口AssetsPackages两个文件夹
    • Assets里放入项目的所有工程文件
    • Packages里查看该项目所拥有的所有资源包
  • 复制文件使用ctrl + D,删除文件使用Delete
  • 多选文件时,按ctrl键加选减选
  • 连选文件时,先选择首文件,按shift键选择尾文件

Inspector细节清单

  • Inspector窗口用于显示在Hierarchy窗口Project窗口中选择的物体的内部信息
  • 如果选择Hierarchy窗口中物体:(拿场景中Main Camera举例)
    • Inspector窗口中显示三个标题字体加粗的区块:TransformCameraAudio Listener
    • 这三个区块被称为组件
    • 所有组件之上,可以设置该物体的Name名称Tag标签Layer层级Static是否静态
    • 所有组件之下,按钮Add Component可以为该物体添加新组件
    • 几乎任何物体都拥有Transform组件
  • 如果选择Project窗口中文件:
    • 如果该文件是预制体,则会显示与选择Hierarchy窗口中物体的显示大致一致
    • 如果该文件是C#脚本或其他文本文件,则会显示脚本文字的预览
    • 其他文件都有其特殊的显示

Console日志窗口

  • 通过标签栏->Window->General->Console或Ctril + Shift + C打开Console窗口
  • Console窗口用于显示游戏日志,包括游戏bug、警告及玩家测试语句

🚩脚本功能

  • 在Project窗口里新建文件夹用来装脚本

新建脚本

  • 在文件夹内右键->Create->C# Script来创建脚本,同时为脚本命名
  • 如果不命名,默认名为NewBehaviourScript,同时不建议在Project窗口里改脚本名
  • 双击打开新建的脚本(默认下载了VisualStudio并安装了Unity依赖项)
  • 新建的脚本如下:
    • 如果脚本类需要改名字,则需要保证脚本内类名与Project窗口中脚本文件名一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Collections;//引用非泛型集合
using System.Collections.Generic;//引用泛型集合
using UnityEngine;//引用Unity引擎依赖项

public class MyScript_1 : MonoBehaviour//继承自MonoBehaviour类
{
void Start()
{
//Start方法只在游戏开始时第一帧执行
}
//
void Update()
{
//Update方法在游戏开始后每一帧执行
}
}

组件挂载

  • 继承自MonoBehaviour类的脚本属于组件脚本,可以挂载到任何游戏物体上来作为组件
    • 保存脚本后,Unity会自动编译,同时引擎右下角会转圈
    • Hierarchy窗口中右键新建一个空物体(Create Empty),命名为GameObject_1
    • Project窗口MyScript_1脚本拖到此新建游戏物体的Inspector窗口下方空白处
    • 于是GameObject_1物体就拥有了MyScript_1脚本组件
    • 右键组件标题栏->Remove Component用于移除该组件
  • 一个物体可以添加多个组件,组件与组件之间是平行关系,没有顺序限制
  • 预制体也可以重载添加组件,但重载的组件不允许放置在已预置组件之前
  • 物体身上的多个相同组件默认以放置的顺序来排序
  • 如果组件拥有像Start()Update()类似的线程函数,在Inspector窗口会显示对勾
    • Inspector窗口左上角的对勾用于控制该物体是否显示

Mono类的生命周期

  • 在继承自MonoBehaviour类的脚本中,内置了许多线程函数,它们的调用拥有先后顺序(生命周期)
    • 官方绘制的周期图:网页
    • 在下图(不完整)中,蓝色标注的是常用函数,粉色标注的是事件函数

🚩脚本通信

  • 游戏是由许许多多逻辑回圈构成的,这种逻辑回圈被称为系统
    • 如数值系统、动画系统、联机系统、界面系统等
  • 上述系统都属于功能较为完整单一的逻辑回圈,各司其职、相互访问数值,从而构成游戏
  • 逻辑回圈中最重要的就是脚本通信
  • 最基本的脚本通信逻辑即获取(Get)到其他脚本内部的数值,或直接设置(Set)它们

Get和Set

  • Get和Set的来历:
    • 在类定义时,私有private受保护protected的变量无法被外部脚本直接获取
    • 这时就需要设置公开public成员函数来间接进行GetSet操作

组件访问

  • 在Unity中,对于相对静态的物体身上脚本组件的访问大致拥有几种类型:

    • 一:在Inspector窗口拖拽

    • 二:先获取物体,再获取脚本组件

  • 如何查看组件的真实类名:

    • 在任何不熟悉的组件的标题栏上右键->如果没有Edit Script选项:
      • 此组件为Unity官方组件
      • 此组件类的类名与该加粗标题名一致
    • Camera组件没有Edit Script选项,所以可判断该组件类名为Camera
    • TextMeshPro-Text(UI)组件有Edit Script选项,查看其类名为TextMeshProUGUI

物体组件访问函数

  • 因为组件都是挂到物体上的,所以需要先获取物体,再获取其身上的组件
  • 获取语句建议写在Start()方法里
  • 获取物体的方法:
    • 直接搜索名字,但是需要保证没有重名物体
      • GameObject GameObject.Find()
    • 将物体设置某个标签Tag,但是需要保证只有该物体有该标签
      • GameObject GameObject.FindGameObjectWithTag()
      • GameObject[] GameObject.FindGameObjectsWithTag()
      • GameObject GameObject.FindWithTag()
  • 获取物体中组件的方法:
    • 直接获取
      • <Template T> T GetComponent<T>()
      • <Template T> T[] GetComponents<T>()
    • 通过父子物体获取
      • Component Component.GetComponentInChildren()
      • Component[] Component.GetComponentsInChildren()
      • Component Component.GetComponentInParent()
      • Component[] Component.GetComponentsInParent()

参数的访问

  • print()Debug.Log()一样,都是输出一条消息给Console窗口
  • 比如说要获取和控制摄像机组件身上的视场角
    • 需要先获取场景中的摄像机物体,这里使用拖拽的方法
    • 查看摄像机组件里的参数
      • 注意,这里的参数是被界面脚本加工过的,有时与脚本内部值不匹配
      • 建议到官网API或Manual上查询
    • 根据代码提示找到cam.fieldOfView接口
    • 尝试GetSet
  • 保存代码,运行游戏,打开Console窗口查看数值波动
  • 在运行过程中修改MyScript_1组件暴露的fov参数(拖拽)
    • Game窗口显示的视场发生了变化,Camera组件中FieldOfView参数动态参与改变

小结

  • 以上即基础的参数访问,同时也实现了两个类(CameraMyScript_1)之间的通信
  • 使用代码分流(if-else/switch)及以上介绍的脚本通信,足以实现基础逻辑回圈制作
  • 脚本通信是整个Unity学习的梗概,其他的东西其实都是万变不离其宗的

🔥组件基础

  • 组件的访问是在获取到装载组件的物体后,对其物体使用GetComponent<组件类名>()模板函数
  • 组件的添加是在物体上使用gameObject.AddComponent<组件类名>()模板函数

🔥属性与参数暴露

🚩常用访问修饰符

  • public公开
    • 公开的属性能够被其他脚本所访问,并暴露在Inspector面板
  • private私有
    • 私有的属性只能在该脚本中访问(无修饰符变量默认是私有的)
  • protected保护
    • 保护的属性只能在该脚本和继承该脚本的脚本所访问

🚩常用数值属性

  • 这些都是Unity自带的参数属性,如果对于检视面板美化感兴趣,可以查看另一篇讲Odin插件的文章

  • 携带属性:[RequireComponent(typeof(脚本类名))]

    • 修饰类的属性,可以使物体添加该脚本时强制添加某附着脚本
    • 如果原先有该附着脚本,则锁定该附着脚本
  • 添加到组件工具集:[AddComponentMenu("分栏名")]
    • 修饰类的属性,可以使该脚本添加到Unity的组件工具集中
  • 小标题:[Header("标题名")]
    • 修饰变量的属性,为其修饰的public变量及以下变量添加小标题
    • 该属性下的第一个变量一定得是public修饰变量
  • 可序列化和于细节面板隐藏:[SerializeField] & [HideInInspector]
    • SerializeField能无视private修饰,直接暴露到编辑器,见下图
    • 但由于private的缘故,无法在其他脚本内被访问
    • HideInInspector能将一切被修饰变量于细节面板隐藏,无视public
    • 但由于public的缘故,还是能够在其他脚本内被访问
  • 文本区、空白区、数值范围:[TextArea()] & [space()] & [Range()]
    • TextArea提供一个比较大的范围的文本输入区,可以用于查看文字
    • 需要填入最小和最大显示行数,超过最大行数会显示滚动条
    • TextArea于Multiline属性相似,都是拓展文本区
    • Space单纯就是空白
    • Range为数值类型提供了限制,需要输入最小值和最大值
  • 类序列化:[System.Serializable]
    • 将类序列化成结构体,同样适用于结构体序列化
    • 暴露在细节面板中是以下拉菜单的形式存在的
  • 小提示:[Tooltip()]
    • 鼠标放在细节面板的变量上,会出现小提示

🔥预制体

  • 类似于虚幻的蓝图类,但是Unity的预制体内容较为单薄
  • 预制体就是预先设计好的,存储成文件的,允许携带层级关系的一套物件单例

🚩预制体制作

  • Hierarchy大纲里的单个物体或物体父级拖入Project项目文件窗口中,即可创建
  • 只有在Project窗口中的才是预制体,Hierarchy窗口里的是预制体实例
  • 删除预制体实例,预制体不会受影响;删除预制体,预制体实例会失联
  • 修改预制体参数,预制体实例内的参数也会发生改变
  • 右键预制体实例选择Break Prefab,即可与预制体断开连接,使实例变为普通场景物体

🚩预制体实例化

  • 预制体属于文件,同样也属于物体,同样可以通过GameObject类型来获取
  • 使用GameObject.Instantiate()方法在游戏一开始生成预制体实例

🔥方位运动

🚩DeltaTime秒每帧

  • 如果要在游戏中控制人物的移动,实现逻辑是在Update()方法里每帧对人物方位进行修改
  • 假设说玩家的速度很快,达到了惊人的1米/帧
  • 玩家的电脑性能很好,帧速率(每秒刷新次数)达到了惊人的60帧/秒
  • 通过计算,这位玩家1秒钟就能前进60米
  • 另一个玩家电脑性能很烂,帧速率(每秒刷新次数)达到了20帧/秒
  • 通过计算,这位玩家1秒钟才能前进20米
  • 在游戏人物速度一致的情况下,仅通过电脑性能就能使两个联机的玩家在速度上拉开差距
  • 这是不可取的,所以要引入秒每帧(增量时间)DeltaTime的概念
  • 尝试使用1去除帧速率,60帧/秒的玩家的增量时间为1/60,20帧/秒的玩家为1/20
  • 然后在玩家速度上乘以这个变量
  • 60帧/秒的玩家最终速度为60*(1/60)=1米/秒,1秒钟前进1米
  • 20帧/秒的玩家最终速度为20*(1/20)=1米/秒,1秒钟前进1米
  • 虽然速度大幅下降,但是两位玩家达成了速度的归一化,排除了硬件影响
  • 所以说,DeltaTime拯救了高ping玩家
  • 在Unity里,使用Time.deltaTime来获取增量时间

🚩移动旋转缩放

  • 一般情况下,需要乘上速度和增量时间

🔥输入事件

  • 输入由Input接口去管理
  • 主要有以下几种输入事件:

🚩鼠标事件

  • Input.GetMouseButton() 鼠标持续按下状态
  • Input.GetMouseButtonDown() 鼠标按下状态
  • Input.GetMouseButtonUp() 鼠标抬起状态
  • Input.MousePosition 鼠标位置
  • Input.mouseScrollDelta 鼠标滚轮状态

🚩键盘事件

  • Input.GetKey() 键盘持续按下状态
  • Input.GetKeyDown() 键盘按下状态
  • Input.GetKeyUp() 键盘抬起状态
  • Input.anyKey 任意键持续按下状态
  • Input.anyKeyDown 任意键按下状态

🚩轴事件

  • 在Unity的Project Settings项目设置窗口里的Input Manager设置按键轴参数
  • 通过轴名称的字符串来获取轴按键输入
  • 里面的参数自行翻译,没什么好说的
  • Input.GetAxis() 获取轴输入,返回浮点值

🚩轴按键事件

  • 轴输入获取的是轴的数值状态

  • 要获取Input Manager里自定义按键的按钮事件,就得使用按键事件

  • Input.GetButton() 按键持续按下状态

  • Input.GetButtonDown() 按键按下状态

  • Input.GetButtonUp() 按键抬起状态


🚩手柄事件

  • 手柄输入分为手柄按键和手柄轴两个部分:
    • 手柄按键:Input.GetKey/Down/Up(KeyCode.JoystickButton1~19)
    • 手柄输入:Input.GetAxis()
  • 手柄按键输入具体要按照手柄品牌和型号而定,下图是Xbox版本
  • 手柄输入需要在Project Settings里设置,主要设置:
    • Dead忽视阈值,输入不达阈值则看作没输入
    • Sensitivity灵敏度,轴返回值是-1~1之间的数,再乘上灵敏度
    • Invert是否翻转轴
    • Axis具体哪个轴,见下图所示

🔥接触事件

  • 在场景里新建一个Cube方块,在Inspector面板里可以看到BoxCollider箱体碰撞箱组件
  • 碰撞和触发事件就是通过此组件进行的
  • 将我们的脚本MyScript_1附着在此方块上
  • 接触事件都有2d和3d两个版本

🚩碰撞事件

  • 碰撞事件产生的前提条件是碰撞的双方至少有一方拥有刚体Rigidbody组件
  • 产生碰撞事件需要将BoxCollider组件上的is Trigger取消勾选
  • 碰撞事件与初始化、更新事件同级,都属于事件方法
  • 当物体产生碰撞时会自动调用碰撞方法,并返回Collision类型的参数

🚩碰撞层级

  • Project Settings里找到Tags and Layers,添加两个自定义层级
  • 将两个需要碰撞的物体分别设置成不同的碰撞层
  • 别忘了碰撞箱组件和刚体组件
  • Project Settings里找到Physics,翻到最底下的Layer Collision Matrix
  • 横轴纵轴之间交叉的勾选框即代表两个层级的物体之间是否能够发生碰撞

🚩触发事件

  • 触发事件产生的前提条件同样是是触发的双方至少有一方拥有刚体Rigidbody组件

  • 与碰撞事件类似,触发事件只需将需要检测触发的BoxCollider组件的is Trigger勾选上即可

  • 任何一方勾选is Trigger都可以触发触发事件


🔥射线检测

  • 射线检测就是从一个点到另一个点之间拉一条红外线,经过的物体会触发射线检测

🚩直线射线检测

  • 最简单的射线检测
  • 对射线做出一些限制,射线检测允许使用物体层级来区分
  • 如何检测一堆物体

🚩圆弧射线检测


🔥物理运算

🚩刚体

  • Unity的绝大多数物理运算都与刚体组件Rigidbody有关
  • 作为引导类的文章,只介绍为什么要使用刚体,以及一些基础操作
  • 在之前的碰撞检测里面以及提及了刚体在碰撞事件中的作用,所以只需要简要介绍一下其他内容:
    • Mass:质量,质量越大惯性越大,推动它也需要更多的力
    • Drag:阻力,移动时,物体会受到一种类似空气阻力的反作用力
    • Angular Drag:旋转阻力,旋转时所受到的阻力
    • Use Gravity:是否开启重力,不开启重力会失重,凡受力之后会永远匀速运动下去
    • Is Kinematic:是否开启动力学,不开启时不会受力移动,适合用于一些特殊情况
    • Constraints:限制,允许冻结移动或旋转的轴向

🔥界面绑定

  • Unity里的界面是使用一种与三维共存的方式来运行的,在使用界面之前,需要在Hierarchy大纲里右键新建一个Canvas画布,同时会创建一个EventSystem物体,用来管理控制行为
  • 右键Canvas创建的一切物体都隐藏了其Transform组件,取而代之的是RectTransform组件
  • 双击Canvas让视图居中,点击Scene窗口左上方的2D视图按钮,用来方便查看
  • Pos XPos Y都指的是屏幕坐标,如果Game窗口里定义的大小是1920*1080像素,那么如果想要将物体贴于屏幕右上角,它的坐标就会是(960,540),因为坐标原点是屏幕中心
  • widthheight是图像宽和高,单位像素
  • Anchors是物体锚点,位于0到1之间,锚点用来规定图像的延展区间,以防由于输出分辨率的变化,界面元素显示错误的问题
  • 锚点的用处在于:无论分辨率如何,物体与锚点之间的距离恒定
  • 点击组件左上角的锚点预设图标,允许选取锚点预设
  • 锚点预设能将物体锚点设置在屏幕的多个位置,但不会修改物体的实际位置
  • 在选取锚点预设时按住alt键,就能在修改锚点的时候修改物体位置

🚩编码提示

  • 在使用Unity的界面之前,需要做上引用:using UnityEngine.UI;

  • 最常见的界面元素是文本,也就是Text组件,在代码中获取组件后,便可以修改其中text参数,也就是显示的文本了

  • 图像是Image组件,其中图像参数是sprite

  • 单选框是Toggle组件,其中是否开启参数是isOn

  • 滑动条是Slider组件,其中滑动数值参数是value


🚩按钮与事件绑定

  • 按钮是Button组件,组件最下方有一个OnClick()点击事件接口,点击右下方的加号用来添加函数槽

🔥美术与程序

  • 因为美术的知识太繁杂,也不好描述,这里只讲美术与程序之间的调用

🚩材质与默认着色器

  • 着色器Shader材质Material的爹,一个着色器可以拥有多个材质的实例,着色器控制材质的渲染方法与参数,着色器就是材质的源码,在Unity里,官方提供了一个叫Standard Shader的基础着色器

  • 将材质拖给任何携带类似Renderer组件的物体时,能够直接赋予上去

  • 在Project窗口里右键新建一个材质,在Inspector窗口中是这样显示的

  • 右键Inspector窗口中的第一栏,选择Select Shader用来查看默认着色器参数
  • Properties下面,左边一栏是参数在其源码中的名称,意思是,如果在代码里调用着色器参数,只能调用这种名称

🚩粒子

  • Hierarchy窗口里右键新建Effects->ParticleSystem创建粒子系统
  • 其中Play On Awake是是否在启动时运行
  • 太多了,这些东西需要上手去实操,其他的有时间再聊

🔥数据持久化

  • 数据持久化就是存档,游戏不能没有存档
  • 除了Unity自带的PlayerPrefs方法,还有JsonXMLbit等方法也能更好的存档

🚩PlayerPrefs


🔥场景跳转与退出

  • 点击工具栏的File->BuildSettings,查看游戏构建设置
  • 将需要参与场景跳转的所有场景(就是最终输出的游戏场景)拖入Scenes In Build栏位里
  • 在代码中需要引用using UnityEngine.SceneManagement;
  • 代码比较简单