Unity初级

一、Unity简介

1. Unity界面

Shift + Space : 放大界面

  • Scene
    • 界面按钮
      • 渲染模式
      • 2D/3D
      • 光源
      • 声音
      • 各种显示开关
      • 场景中元素图标的显示开关(Gizmos)
      • 搜索框
    • 界面操作
      • 缩放
      • 旋转
      • 移动
      • QWERTY快捷键
  • Game
    最终给玩家看到的界面,由场景中的摄像机拍摄到的画面
    • 界面按钮
      • Display 通道
      • 分辨率
      • 缩放Scale:将摄像机拍摄到的画面缩放
      • 播放时最大化
      • 静音
      • 状态信息面板开关
      • 场景中元素图标的显示开关(Gizmos)
  • Hierarchy(层级面板)
    能够看到场景中所有的层级对象,与其层级关系
    • 新建
    • 搜索
  • Project (项目面板)
    • 一列展示 / 两列展示
    • 与硬盘中的/Project/Assets文件夹同步
  • Inspector(属性面板)
    • 作用
      • 选中游戏对象后,在面板看到物体身上的组件
      • 看到一个资源文件的属性
  • Console(控制台面板)
    • 按钮
      • Clear:清空
      • Collapse:合并重复项
      • Clear on Play:运行时清空上一次的日志
      • ErrorPause:当程序遇到错误(异常)时暂停
  • AssetStore(资源商店)
    • 修改语言
    • 价格以美元做单位

2. 坐标系

  • 世界坐标系
    “东西南北”
  • 物体坐标系
    “前后左右”
  • 中心点 Pivot/Center 快捷键Z
  • 坐标系 Global/Local 快捷键X

3. 材质球(Material)

想要修改一个游戏对象的外观,需要使用材质球

4. 资源包(Assets package)

*路径和名字不要有中文

  • 导入 Import
  • 导出 Export
  • 标准资源包 Standard Assets
  • 天空盒使用的两种方式
    • Window -> Lighting -> Settings
    • 给摄像机加组件Skybox

5. 预设体(Prefab)

  • 设置
    • Select
    • Revert
    • Apply

6. 地形系统(Terrain)

  • 上升/下降地形
    最低高度为0
  • 恒高地形
  • 平滑地形
  • 绘制地面纹理
  • 绘制树木
    • 可以添加多种树
    • 笔刷大小
    • 树木密度
    • 锁定宽高比
    • 删除
      • 按住shift:删除范围内全部
      • 按住ctrl:删除范围内选择的树木
  • 绘制草坪
    • 删除
      同删除树木
  • 设置
    • 长宽高

7. 向量

  • 标量
    只有大小没有方向
  • 基本运算
    • 向量的加减
      • 加法:将两个向量首尾相连,和等于第一个向量的头到第二个向量的尾这个向量
      • 减法:等于被减数的终点到减数的终点这个向量
    • 向量的点乘
    • 向量的叉乘
    • 向量的几何含义

8. 2D 项目与 3D 项目

  • 2D与3D项目区别

    • 2D没有天空盒
    • 2D没有默认光源
    • 摄像机的成像模式2D是正交视图,3D是透视图
  • 将动画中的所有Sprite帧选中,拖到Hierarchy中就自动生成了一个精灵动画

  • 组件

    • Transform
    • Sprite Renderer:精灵图渲染器
      • Sprite:素材图片
      • Color:颜色
      • Flip:翻转
        • FlipX:水平翻转
        • FlipY:垂直翻转
      • Material:材质
      • Sorting Layer:在哪个层排序
      • Order In Layer:决定谁在屏幕前,数字越大越靠前
    • Collider2D
    • Rigidbody2D
    • PhysicsMaterial2D
      • Edit -> Project Settings -> Physics2DSettings 修改默认物理材质

二、Unity脚本

  • 脚本在创建后名称不能被修改
  • 脚本可以当做一个组件来使用
  • 脚本是一个类,继承自MonoBehaviour
  • 脚本文件的名称和类名一定要保持一致,否则会不能将脚本挂在GameObject
  • 继承关系:GameObject -> Component -> Behaviour -> MonoBehaviour -> Scripts
    • 此GameObject不是彼Object,是UnityEngine定义的GameObject
  • Runtime/Editor 脚本
    • Runtime脚本,执行随着游戏运行时进行而运行
    • Editor脚本,执行在游戏运行前

Unity脚本的生命周期

  • Unity中什么都是对象
  • 脚本的声明周期:一个脚本对象从最开始执行到最后销毁的全部过程
  • Unity中的打印
    • Debug.Log("Message");
    • print("Message"); //不是脚本不能用此方法
  • 脚本前的对勾:
    • 脚本是否激活。依赖于Start()和OnEnable()
  • 生命周期
    • void Awake():游戏开始后第一个调用的回调函数。无论脚本是否激活,始终都执行。如果游戏对象未激活,他身上所有的组件是不会触发的
    • void Start():游戏开始后第一帧执行的回调函数,只会执行一次
    • void OnEnable():当脚本激活的时候触发一次,可以重复触发
    • void OnDisable():当脚本取消激活的时候触发一次,可以重复触发
    • void OnDestroy():当脚本被销毁前触发。脚本被移除或者游戏对象被销毁触发。游戏对象在销毁前会移除身上所有的组件,所以就是脚本被移除的时候触发
    • void Update():每帧触发一次
    • void LateUpdate():每帧触发一次,晚于Update()
    • void FixedUpdate():每隔固定的时间间隔触发一次,时间间隔默认0.02s,在Edit->Project Settings->Time中修改
    • void OnGUI()(基本不用了):每帧触发两次
graph LR
Awake-->OnEnable
OnEnable-->Start
OnDisable-->OnDestroy
Start-->Update
Update-->LateUpdate
LateUpdate-->Update
LateUpdate-->OnDisable
Start-->FixedUpdate
FixedUpdate-->FixedUpdate
FixedUpdate-->OnDisable
  • 先挂的脚本后执行,后挂的脚本先执行。执行顺序可以在Inspector->Execution Order中设置,Default Time数值越小的先执行

三、常用类

1. GameObject类

  • GameObject 是一个用来描述游戏对象的类
  • gameObject 表示游戏对象,脚本挂给谁,gameObject就是谁

常用属性与方法

  • gameObject.name 设置/获取一个游戏对象的名字
  • gameObject.tag 设置/获取一个游戏对戏的标签
    • Inspector -> Tag -> Add TagEdit -> Project Settings -> Tags and Layers 管理标签
    • 标签可以用来查询获取游戏对象
    • 注意:设置的Tag值需要是预先设置好的tag
  • gameObject.layer 设置/获取一个游戏对象的层
    • int型 代表层的序号
    • LayerMask 结构体
      • LayerMask.NameToLayer() 通过层名字获取层号
        gameObject.layer = LayerMask.NameToLayer("abc");
      • LayerMask.LayerToName() 通过层号获取层名字
        gameObject.layer = LayerMask.LayerToName(8);
    • Inspector -> Layer -> Add LayerEdit -> Project Settings -> Tags and Layers 管理层
    • 层是有序号的,前8个不能更改,是系统自定的
    • 层的作用:
      • 场景中可以分层显示
      • Main Camera -> Culling Mask 分层获取图像
      • 指定层接收/屏蔽射线
  • 游戏对象的激活
    • 获取游戏对象的激活状态 bool active = gameObject.activeSelf;
    • 设置一个游戏对象的激活状态 gameObject.SetActive(false);
  • 查找游戏对象
    • 通过名字查找 GameObject obj = GameObject.Find(string name);
      • 名字不能打错
      • 同名的时候,后创建的先找到,返回找到的这个游戏物体
        GameObject obj = GameObject.Find("/Sphere/Sphere"); 通过路径查找
    • 通过标签查找
      • GameObject obj = GameObject.FindWithTag(string tag); 找到一个,后创建的先找到
      • GameObject[] obj = GameObject.FindGameObjectsWithTag(string tag); 找到所有这个标签的游戏物体
      • 推荐使用tag查找,效率高
    • 通过组件类型查找
      • SphereCollider[] colliders = GameObject.FindGameObjectsOfType<SphereCollider>();
    • GameObject中所有的查询方法,都不能找到非激活的物体
  • 获取组件
    • BoxCollider collider = gameObject.GetComponent<BoxCollider>(); 获取组件
    • BoxCollider collider = gameObject.GetComponentInChildren<BoxCollider>(); 从自己开始往子物体中找,获取组件。如果自己有,选中自己的;自己没有,选中子物体的
    • collider.enabled = false; 设置组件的激活状态
  • 添加组件 BoxCollider boxCollider = gameObject.AddComponent<BoxCollider>();
    • 添加脚本 gameObject.AddComponent<ScriptName>();
  • 销毁游戏对象或者组件
Rigidbody rigi = gameObject.GetComponent<Rigidbody>();
Destroy(rigi);      // 直接销毁
Destroy(rigi, 3);   // 延迟3秒销毁

Destroy(gameObject, 2);     // 2秒后销毁游戏对象

2. Mathf 类

常量 作用
Mathf.Rad2Deg 弧度转角度 degrees = radian * Mathf.Rad2Deg
Mathf.Deg2Rad 角度转弧度 radian = degrees * Mathf.Deg2Rad
  • 角度:360° = 弧度:2π
  • 反三角函数求出的都是弧度
方法 作用
Mathf.Abs 绝对值
Mathf.Pow 指定次幂
Mathf.Sqrt 开平方
Mathf.Sin 正弦值
Mathf.Cos 余弦值
Mathf.Tan 正切值
Mathf.Asin 反正弦
Mathf.Acos 反余弦
Mathf.Atan 反正切,一个参数
Mathf.Atan2 反正切,两个参数 对边和临边的值
Mathf.Floor 向上取整
Mathf.Round 四舍五入
Mathf.Ceil 向下取整
Mathf.Clamp(int value, int min, int max) 取限制值:如果小于min返回min,如果大于max返回max,否则返回value
Mathf.Lerp(float a, float b, float t) 取插值:a和b可以任意设置,t的取值范围是[0, 1],返回a + (b - a) * t

Mathf.Lerp不是为了求一个具体的插值,而是一个趋势,t越小,越趋近于a,t越大,越趋近于b

3. Random 类

求随机数

取[min, max)范围内的整型随机数
Random.Range(int min, int max);
取[min, max)范围内的浮点型随机数
Random.Range(float min, float max);

4. Time 类

属性 作用
Time.time 获取游戏开始到当前的时间
Time.deltaTime 获取两帧的时间差(每帧的时间)
Time.FixedDeltaTime 获取TimeStep
Time.timeScale 获取/设置Unity和现实世界的时间比例(默认1:1)用来改游戏速度

5. Vector3 结构体

表示一个三维空间中的点的坐标,或者表示一条三维向量

// 实例化一个
Vector3 a = new Vector();   // 默认x,y,z都是0
Vector3 a = new Vector(1, 2);   // x=1, y=2, z=0
Vector3 a = new Vector(1, 2, 3);   // x=1, y=2, z=3

5.1 Vector3的常量

常量
Vector3.zero (0, 0, 0)
Vector3.one (1, 1, 1)
Vector3.up (0, 1, 0)
Vector3.down (0, -1, 0)
Vector3.right (1, 0, 0)
Vector3.left (-1, 0, 0)
Vector3.forward (0, 0, 1)
Vector3.back (0, 0, -1)

x轴正方向为右,y轴正方向为上,z轴正方向为前

5.2 Vector3中的运算符重载

  • + :x相加 y相加 z相加得到新的点
  • - :x相减 y相减 z相减得到新的点
  • * :例 p * 4 是x/y/z分别乘4 得到新的点
  • == :判断是否相等
  • != :判断是否不等

5.3 Vector3的属性

属性 作用
p.normalized 求单位向量(标准化向量/向量归一化)
p.sqrMagnitude 求向量的模的平方(向量的模:向量的长度)
p.magnitude 求向量的模

5.4 Vector3的方法

方法 作用
Vector3.Angle(p0, p1) 求两个向量的夹角,返回一个角度
Vector3.Distance(p0, p1) 求两个点的距离
Vector3.Cross(p0, p1) 求两个向量的叉乘
Vector3.Dot(p0, p1) 求两个向量的点乘
Vector3.Lerp(Vector3 a, Vector3 b, float t) 求插值

6. 调试

  • 划线调试
// 在两点之间划线
Debug.DrawLine(Vector3 start, Vector3 end);
Debug.DrawLine(Vector3 start, Vector3 end, Color color);
Debug.DrawLine(Vector3 start, Vector3 end, Color color, float duration);
// 从p点开始向direction方向发射射线,长度可以固定
Debug.DrawRay(Vector3 start, Vector3 direction);
Debug.DrawRay(Vector3 start, Vector3 direction, Color color, float duration);
  • 日志调试
Debug.Log(string message);
Debug.LogWarning(string message);
Debug.LogError(string message);

7. Transform 类

表示一个物体的位置旋转缩放,维系一个物体的父子关系

// 获取Transform组件
Transform t = GetComponent<Transform>();
gameObject.transform
transform
  • 属性
属性 作用
position 物体的世界坐标(世界坐标)
localPosition 物体相对于父物体的坐标(物体坐标)
eulerAngles 欧拉角旋转(世界坐标)
localEulerAngles 欧拉角旋转(物体坐标)
rotation 四元数旋转(世界坐标)
localRotation 四元数旋转(物体坐标)
localScale 物体的缩放
parent 得到父物体的transform
root 得到根物体的transform
childCount 返回子物体的个数
  • 常量
常量
forward 物体自己的z轴正方向(前方)
right 物体自己的x轴正方向(右方)
up 物体自己的y轴正方向(上方)
  • 方法
方法定义 作用
transform.Translate(Vector3 v3) 只移动一次(物体坐标)
transform.Translate(Vector3 v3, Space relativeTo) 只移动一次(Space坐标)
transform.Rotate(Vector3 axis, float angle) 沿轴axis,旋转angle角度(自转)(物体坐标)
transform.RotateAround(Vector3 point, Vector3 axis, float angle) 绕着point,沿轴axis,旋转angle角度(公转)
transform.LookAt(Transform transform) z轴正方向指向某个物体
transform.SetParent(Transform transform) 设置父物体
transform.GetChild(int index) 获得子物体(index是子物体的序号)
transform.Find(string name) 获得子物体(通过子物体的名字)
transform.GetSiblingIndex() 获取物体在父物体中的下标
transform.SetSiblingIndex(int index) 修改物体在父物体中的下标
  • transform中所有的查找方法可以找到非激活的
// 速度每秒10米 用10 * Time.deltaTime
transform.Translate(Vector3.right * 10 * Time.deltaTime);        // 沿物体坐标
transform.Translate(Vector3.right * 10 * Time.deltaTime, Space.World); // 沿世界坐标

// 获取主摄像机的Camera组件
// 主摄像机:具有MainCamera的tag值的物体
Camera c = Camera.main;

8. Quaternion 类

  • 欧拉角转四元数
Quaternion rotation = Quaternion.Euler(Vector3 euler);
Quaternion rotation = Quaternion.Euler(float x, float y, float z);
  • 四元数转欧拉角
Vector3 euler = rotation.eulerAngles;
  • 空旋转
Quaternion.identity
  • 插值函数
Quaternion.Lerp(Quaternion a, Quaternion b, float t);
  • 将一个向量转成一个四元数,用来取方向
Quaternion.LookRotation(Vector3 forward);
  • 向量右乘(Quaternion * Vector3)
    几何含义:将向量旋转指定的四元数角度
Vector3 result = Quaternion.Euler(0, 90, 0) * Vector3.forward;
// result是 Vector3.right (1, 0, 0)

9. Input 类

9.1 键盘、鼠标

  • 检测按键一定不能写在FixedUpdate()中,检测是每一帧检测一次,FixedUpdate可能一帧触发多次
  • 获取鼠标在屏幕上的位置
    • 屏幕坐标是左下角为(0, 0, 0)点,右上角为(Screen.width, Screen.height, 0)点
    • Screen.width屏幕宽Screen.height屏幕高
Vector3 mousePosition = Input.mousePosition;
  • 点击鼠标键盘
// KeyCode是一个枚举,包含几乎所有按键
bool isPress = Input.GetKey(KeyCode key)      // 按住按键持续触发
bool isUp = Input.GetKeyUp(KeyCode key)    // 按键抬起的时候触发一次
bool isDown = Input.GetKeyDown(KeyCode key)  // 按键按下的时候触发一次
bool isDown = Input.GetKeyDown(string name) // 容易写错,不推荐

// 专门检测鼠标按键的,用button数字代表按钮
Input.GetMouseButton(int button)
Input.GetMouseButtonUp(int button)
Input.GetMouseButtonDown(int button)

9.2 Input虚拟轴

  • Horizontal轴:水平
  • Vertical轴:垂直
  • Mouse X轴:鼠标在水平方向上的偏移量
  • Mouse Y轴:鼠标在垂直方向上的偏移量
  • Mouse ScrollWheel轴:鼠标滚轮的偏移量
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
// 容错判断
if (Mathf.Abs() < 0.1f || Mathf.Abs() < 0.1f) return;
// 做一个方向向量
Vector3 direction = new Vector3(horizontal, 0, vertical);
if (direction != Vector3.zero)
{
    // 得到目标方向
    Quaternoin target = Quaternion.LookRotation(direction);
    // 旋转
    transform.rotation = Quaternion.Lerp(tranform.rotation, target, Time.deltaTime * speed);
    // 移动
    transform.translate(Vector3.forward * speed * Time.deltaTime);
}

父物体想要放在子物体中心:

  1. 将父物体作为子物体的子物体
  2. reset transform
  3. 再将子物体作为父物体的子物体

10. Application 类

都是静态的,用Application.xxxx

  • runInBackground:游戏是否支持后台运行
  • dataPath:返回当前工程的Assets文件夹路径
  • persistentDataPath:根据发布的平台,返回一个持久化路径
  • streamingAssetsPath:返回一个StreamingAssets文件夹的路径,是Assets文件夹的一个子文件夹,打包后这个文件夹依然存在
  • OpenURL:打开指定的网页
  • Quit():关闭当前应用程序(只有打完包(发布应用程序)后才有效,手游几乎没有)

11. ScreenCapture 类

ScreenCapture.CaptureScreenshot("绝对路径"):截图

  • 使用代码截图
using System;
using System.IO;

// 判断
bool exists = Directory.Exists(Application.persistentDataPath + "/Images");
if (exists == false)
{
    // 创建文件夹
    Directory.CreateDirectory(Application.persistentDataPath + "/Images");
}

// 获取时间
DateTime now = DateTime.Now;
// 格式化为自定义的格式
string name = now.ToString("yyyy年MM月dd日 HH时mm分ss秒");

string filePath = Application.persistentDataPath + "/Images/" + name + ".png";

ScreenCapture.CaptureScreenshot(filePath);
  • 时间格式化(部分)
时间标签 代表内容
yyyy
MM
dd
HH 时(24小时制)
hh 时(12小时制)
mm
ss

12. Scene Manager 类

  • 场景切换:在发起切换的场景中做一个panel,做切换画面,放一个Slider把Handler删除,Interactable取消勾选,动态修改value值为asyncOperation.progress
using UnityEngine.SceneManagement;

// 1. 将需要切换的场景添加到Scenes In Build
// File -> Build Settings -> Scenes In Build 
// 按按钮添加,右键选场景删除
// 拖拽后每一个场景都有一个编号
// 2. 使用SceneManager类来切换
// 同步切换
void SceneManager.LoadScene(string name);    // 通过场景名字切换
void SceneManager.LoadScene(int index);      // 通过场景下标编号切换
// 异步切换
AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(string name);   // 通过场景名字切换
AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(int index);     // 通过场景下标编号切换
// 可以通过asyncOperation拿到切换的进度
// 在Update()中使用进度
private void Update()
{
    if (asyncOperation != null)
    {
        print(asyncOperation.progress);
    }
}
  • DontDestroyOnLoad 静态方法
    此静态方法允许游戏物体在加载任何场景的时候不销毁,只能让根物体不销毁,可以用来管理全局的事务
DontDestroyOnLoad(Object target);

使用此方法存在一个问题,当在A场景生成了DontDestroyOnLoad的游戏物体后,切换B场景,再切回A场景,会出现多次调用该方法,从而无限循环,解决办法如下:

  1. 将需要全局保留的物体放到一个初始场景里,游戏运行第一时间就加载,加载完成再跳到初始页面
  2. 判断该方法是否已经调用过,设一个全局(静态)变量默认False,当运行过一次之后将其变为True,之后每次进入判断True是否调用
  3. 使用静态构造方法,当第一次使用这个类的时候调一次,所以这个静态构造方法全局只会调用一次,在这个构造方法中创建空物体,将此脚本绑定上去,再DontDestroyOnLoad这个空物体,此时这个DontDestroyOnLoad全局只会调用一次,这个脚本也做成了类似单例脚本,使用方便

13. Audio Source 类

用于管理播放声音文件

  • AudioSource组件
    • AudioClip:音源文件
    • Output:混音调音
    • Mute:静音
    • Loop:循环
    • Volume:音量
    • Spatial Blend:使用2D还是3D声音。2D声音没有位置;3D声音有位置,可以调声音曲线
// 获得AudioSource组件
private AudioSource _audio;
_audio = GetComponent<AudioSource>();

// 加载播放音效
AudioClip clip = Resources.Load<AudioClip>(string path);

// 播放一次
_audio.PlayOneShot(clip);

// 设置一个新的要播放的声音片段
_audio.clip = clip;
// 设置其他的
_audio.loop = loop;
// 背景音播放
_audio.Play();

// 死亡第一段
private void Die1()
{
    // 延迟播放第二段
    Invoke("Die2", 0.5f);
}
// 死亡第二段
private void Die2()
{
    
}

14. Invoke 延迟调用

  • Invoke(string functionName, float delay); 延迟调用方法
  • CancelInvoke(string functionName); 取消指定方法的延迟调用
  • Cancel(); 取消所有延迟调用的方法

15. Gizmos 类

  • Gizmos是用于在场景视图可视化调试或辅助设置
  • 所有gizmo绘制需要在脚本的OnDrawGizmos或OnDrawGizmosSelected里完成
  • OnDrawGizmos在每帧调用。所有在OnDrawGizmos中渲染的gizmos都是可见的。OnDrawGizmosSelected仅在脚本附加的物体被选择时被调用。
  • 静态变量
变量名 作用
Gizmos.color 为随后绘制的gizmos设置颜色
Gizmos.matrix 设置gizmo的矩阵用于绘制所有gizmos
  • 静态方法
方法名 作用
Gizmos.DrawCube(Vector3 center, Vector3 size) 绘制立方体,使用center和size参数,绘制一个实心立方体
Gizmos.DrawFrustum(Vector3 center, float fov, float maxRange, float minRange, float aspect) 绘制相机可视区域,用当前的Gizmos.matrix设置它的位置和旋转,参数:center(棱锥体的顶点),fov(视角大小,角度制),maxRange(视野最大距离),minRange(视野最小距离),aspect(宽高比,宽/高)
Gizmos.DrawIcon(Vector3 center, string name, bool allowScaling = true) 绘制图标,图标被命名为name并且被放置在center处。图标的路径需要放在Assets/Gizmos文件夹
Gizmos.DrawLine(Vector3 from, Vector3 to) 绘制线段,绘制一条从from起点到to位置的线段
Gizmos.DrawRay(Ray ray) 绘制射线
Gizmos.DrawRay(Vector3 from, Vector3 direction) 绘制射线,从from开始向direction绘制一条射线
Gizmos.DrawSphere(Vector3 center, float radius) 绘制球体,使用center和radius参数,绘制一个实心球体
Gizmos.DrawMesh(Mesh mesh, Vector3 position = Vector3.zero, Quaternion rotation = Quaternion.identity, Vector3 scale = Vector3.one) 绘制一个网格
Gizmos.DrawWireCube(Vector3 center, Vector3 size) 绘制线框立方体,使用center和size参数,绘制一个线框立方体
Gizmos.DrawWireSphere(Vector3 center, float radius) 绘制线框球体,根据center和radius参数设置线框球体

引用自http://wiki.ceeger.com/script/unityengine/classes/gizmos/gizmos

四、物理引擎

1. 碰撞体 Collider

1.1 属性

  • Edit Collider:编辑碰撞体的大小
  • Is Trigger:是否是触发器
  • Material:物理材质
  • Center:中心点位置
  • Size:缩放比例
  • Radius:半径
  • ...

1.2 碰撞体的作用

  • 碰撞
  • 射线

1.3 回调方法

方法定义 作用
OnMouseEnter() 鼠标进入碰撞体时触发一次
OnMouseOver() 鼠标在碰撞体内停留时持续触发,每帧一次
OnMouseExit() 鼠标离开碰撞体时触发一次
OnMouseDown() 鼠标在碰撞体范围内按下的时候触发一次
OnMouseDrag() 鼠标在拖拽的时候持续触发,每帧一次
OnMouseUp() 鼠标在抬起的时候触发一次
OnMouseUpAsButton() 鼠标在碰撞体范围内抬起的时候触发一次

1.4 获取范围内碰撞体

方法 作用
Physics.OverlapBox() 获得立方体形范围内的所有碰撞体
Physics.OverlapSphere() 获得球体形范围内的所有碰撞体
Physics.OverlapCapsule() 获得胶囊体形范围内的所有碰撞体

1.5 碰撞

  • 碰撞三要素
    • 碰撞双方都有Collider组件,并且不是Trigger
    • 至少有一方有rigidbody组件
    • 双方需要保持相对运动
  • 回调方法
方法 作用
OnCollisionEnter(Collision collision) 碰撞开始时触发一次
OnCollisionStay(Collision collision) 碰撞过程中持续触发,每帧一次
OnCollisionExit(Collision collision) 碰撞结束时触发一次

collision 是碰撞的信息

// 存有所有接触点的数组
Vector3 point = collision.contacts[0].point;
// 获取到和哪一个碰撞体发生碰撞了
Collider c = collision.collider;

1.6 设置物理引擎相关参数

Edit -> Project Settings -> Physic

属性 作用
Gravity 重力
Default Material 默认的物理材质
Layer Collision Matrix 层之间的碰撞矩阵,控制层之间是否能够碰撞

2. 刚体 Rigidbody

完成逼真的物理效果的物理组件

2.1 属性

  • Mass:质量
  • Drag:阻力
  • Angular Drag:角阻力
  • Use Gravity:使用重力
  • Is Kinematic:开启动力学,不受任何力
  • Interpolate:插值
    • none:无
    • interpolate:内插值
    • extrapolage:外插值
  • Collision Detection:碰撞检测模式
    • Discrete:离散检测,精度不高,性能消耗低,检测不到极端情况
    • Continuous:连续检测,提高检测精度,性能消耗大
    • Continuous Dynamic:连续动态检测,提高检测精度,性能消耗大
  • Constraints 约束

2.2 代码中的属性和方法

Inspector面板上显示的属性都可以用代码获取到

属性方法 作用
mass 重量
drag 阻力
angularDrag 角阻力
useGravity 使用重力
isKinematic 开启动力学
interpolation 插值 RigidbodyInterpolation枚举
collisionDetectionMode 碰撞检测模式 CollisionDetectionMode枚举
constraints 约束 RigidbodyConstrains枚举
velocity 速度 Vector3类型(初速度)
angularVelocity 角速度 Vector3类型(初速度)
AddForce(Vector3 force); 给刚体添加一个力
AddExplosionForce(float explosionForce, Vector3 explosionPosition, float explosionRadius) 给刚体添加一个爆炸力(炸自己)。想别的刚体也受到爆炸力影响,需要给别的刚体也添加一个爆炸力

2.3 层检测与层屏蔽

// 已知cube1是第9层,cube2是第10层
// 层检测
LayerMask mask = 1 << 10 | 1 << 9; // 获取第9和10层
LayerMask mask = 1 << LayerMask.NameToLayer("cube1"); // 获取第9层
LayerMask mask = GetMask(params stirng[] layerNames);
// 层屏蔽
mask = ~mask;  // 按位取反mask

3. 触发 Trigger

触发会用到ColliderIsTrigger

  • 触发三要素
    • 两个物体都有碰撞体
    • 至少有一个带有刚体
    • 至少有一个是触发器
  • 触发的回调
回调定义 作用
OnTriggerEnter(Collider collider) 触发开始时触发一次
OnTriggerStay(Collider collider) 触发持续过程中,每帧调用
OnTriggerExit(Collider collider) 结束触发时触发一次

4. 射线 Ray

  • 射线包含起点和方向,Unity中可以限定射线长度(Vector3的长度)
  • 有Collider的物体会被射线打中,碰撞体和触发器都能被触发
  • 系统提供了一个层叫Ignore Raycast,用于忽略射线
// 生成射线
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// 声明一个RaycastHit,用来存储射线打中的物体的信息,**是一个结构体,值类型**
RaycastHit hit;
// 发射射线
// true:打中物体了  false:没有打中物体
bool result = Physics.Raycast(ray, out hit);
if (result)
{
    // 获取射线打中的物体
    hit.collider;
    // 获取射线打中的坐标(世界坐标)
    hit.point;
}
// 发射一条射线,返回射线打中的所有物体
RaycastHit[] hits = Physics.RaycastAll(ray);

5. 动态生成游戏物体

// 生成谁
public GameObject plane;
// 在哪生成
// 旋转是什么
Instantiate(Object obj, Transform parent);  // 游戏对象生成并绑定父物体
Instantiate(Object obj, Transform position, Quaternion q)   // 在指定的位置position使用指定的旋转q生成游戏对象obj
  • 代码读取 Resources 中的Prefab
// 资源路径一定是 /Resources/ 或其子文件夹中,否则读取不到
// 示例中放在了 /Resources/Bullet/bullet1
GameObject _bulletPre;
private void Start(){
    // Load()的返回值是Object,所以要向下转型
    _bulletPre = Resources.Load("Bullet/bullet1") as GameObject;
    // 使用泛型的返回值是 T 类型,可以不用转型
    _bulletPre = Resources.Load<GameObject>("Bullet/bullet1");
    // 加载文件夹中所有的 T 类型资源
    GameObject[] objs = Resources.LoadAll<GameOBject>("Bullet");
}

五、UGUI

  • 用于绘制2D界面,Z轴方向的缩放是无效的
  • 需要引用命名空间using UnityEngine.UI;

1. Text:文本

  • W工具会改变内容的宽高比,T工具不会改变内容的宽高比
  • Rect Transform:是Transform的子类
  • Canvas Renderer:画布渲染器
  • Text(Script)组件
    • Text:显示的文本
    • Character:字体
      • Font:字体
      • Font Style:文字风格
        • 粗体
        • 斜体
        • 粗体+斜体
      • Font Size:字体大小
      • Line Spacing:行间距
      • Rich Text:是否支持富文本(富文本:可以单独修改某些文字的风格)
        例:<b>hello</b> <i>world</i>
    • Paragraph
      • Alignment:对齐方式
      • Align By Geometry:是否对齐到网格
      • Horizontal Overflow:水平方向溢出
        • Wrap:正常
        • Overflow:溢出还显示
      • Vertical Overflow:垂直方向溢出
        • Truncate:正常
        • Overflow:溢出还显示
      • Best Fit:根据文本内容的多少和Text的大小,动态调节文字的大小(条件:需要水平和垂直方向不能是Overflow)
        • Min Size:最小文字大小
        • Max Size:最大文字大小
    • Color:字体颜色
    • Material:字体材质
    • Raycast Target:能否接收射线
  • 可添加组件
    • Shadow:Text阴影
      • Effect Color:阴影颜色
      • Effect Distance:阴影距离
      • Use Graphic Alpha:
    • Outline:字体描边
      • Effect Color:描边颜色
      • Effect Distance:描边距离
      • Use Graphic Alpha:
  • 代码中的设置
// 获取属性
_text = GetComponent<Text>();
// 属性设置
_text.text = "hello world";
_text.font = Resources.Load<Font>(string path); // 通过Resources文件load
_text.fontStyle = FontStyle.Bold;   // FontStyle枚举
_text.fontSize = 14;
// 段落设置
_text.alignment = TextAnchor.LowerCenter; // 枚举
_text.color = Color.Red;
_text.color = new Color(float r, float g, float b, float a);  // rgba颜色

2. Image 与 Raw Image 图片

  • 区别
    • Image显示Sprite,精灵图
    • Raw Image显示Texure,纹理图
  • 图片设置
    • Texture Type:图片类型
      • Default:默认为纹理图
      • Sprite(2D and UI):精灵图
        • Sprite Mode:精灵图模式
          • Single:单图
          • Multiple:多图
  • 组件
    • Image(Script)
      • Source Image:图片源,若代码中想修改,需要使用Image.sprite改图片
      • Color:图片颜色
      • Material:图片材质
      • Raycast Target:是否标记为光线投射目标。默认勾选,表明鼠标无法点击该图片后面的物体;不勾选代表可以点击后面的物体
      • Image Type:填充方式
        • Simple:默认填满整个Image
        • Sliced:图片为单张的时候,除了四个角都拉伸,用于做边框
        • Tiled:平铺
        • Filled:填满
          • Fill Method:填充方式
            • Horizontal:水平方向填充
            • Vertical:垂直方向填充
            • Radial 90:以左下角为圆心,按角度填充
            • Radial 180:以下边中心点为圆心,按角度填充
            • Radial 360:以图片中心点为圆心,按角度填充
          • Fill Origin:填充起始位置
          • Fill Amount:填充比例
          • Clockwise:顺时针逆时针
          • Preserve Aspect:保持宽高比
      • Set Native Size:设置原图大小
    • Raw Image
      • Source Image:图片源,需要sprite 代码中使用Image.texure改图片
      • UV Rect:令图片的一部分显示在Raw Image中,可根据x,y坐标让图片动起来
        • X:图片左下角的坐标x
        • Y:图片左下角的坐标y
        • W: 宽 Width
        • H: 高 Height

3. Button 按钮

  • 有一个子物体Text
  • Button(Scrite)组件
    • Interactable:用户能否交互
    • Transition:在不同的状态间切换的时候显示的效果
      • Normal Color: 按钮实例化后显示的颜色
      • Highlighted Color:鼠标移动到button上显示的颜色
      • Pressed Color:鼠标点击了按钮显示的颜色
      • Disabled Color:禁用按钮时的颜色
      • Color Multipier:颜色切换速度,越大则颜色变化越快
      • Fade Duration:渐变动画的时长,越长越不明显
  • 按钮的点击事件
    • 一定是无返回值的事件
    • 通过代码给一个按钮添加一个点击事件
// 声明一个按钮
private Button _button;
// 获取按钮组件
_button = GetComponent<Button>();

// 给按钮添加一个点击事件(监听)
// UnityAction 是void返回无参的委托
_button.onClick.AddListener(UnityAction call);
// 用 Lambda表达式添加一个点击事件
_button.onClick.AddListener(()=>
{
    Debug.Log("Hello world");
});

// 删除按钮的点击事件
_button.onClick.RemoveListener(UnityAction call);
// 删除按钮的所有点击事件
_button.onClick.RemoveAllListeners();

4. Toggle 选项框

由背景Background(Image),对勾Checkmark(Image),标签Lable组成(Text)

  • Toggle(Script)
    • Transition:渐变
      • None:无渐变
      • Color Tint:使用color为按钮染色,同button
      • Sprite Swap:在精灵图中切换
        • Tartget Graphic:目标图形,默认的是子物体的Background,是一个Image组件
        • Highlighted Sprite:高亮显示的精灵图
        • Pressed Sprite:点击时显示的精灵图
        • Disabled Sprite:禁用时显示的精灵图
      • Animation:使用Toggle controller控制渐变
    • Navigation:选项框导航,用于遥控器或手柄等设备
    • Is On:设置/获取选项框的勾选状况
    • Toggle Transision:效果
      • Fade:淡入淡出效果
    • Group:分组,将想要单选的所有Toggle放在同一个组中,每组中只能勾选一个
      • Toggle Group组件
        • Allow Switch Off:勾选后,可以一个选项都不选,不勾选时,至少选择一项
  • On Value Changed(boolean):按钮的回调方法,勾选状态变更,会触发一次
// 声明一个Toggle组件
private Toggle _toggle;
// 获取Toggle组件
_toggle = GetComponent<Toggle>();
// 添加事件
_toggle.onValueChanged.AddListener(status =>
{
    // 参数status:表示发生变化后,toggle的isOn
    Debug.Log(_toggle.isOn);
    // 或者就使用status
    Debug.Log(status);
});

5. Slider 滑块

  • 由背景background(Image)、Fill Area -> 进度条Fill(Image)、Handle Slide Area -> 滑块Handle(Image)组成
  • 用于场景加载进度、下载进度、调音量等等
  • Slider
    • Rill Rect:进度条的填充,默认使用子物体的Fill
    • Handle Rect:滑块的填充,默认使用子物体的Handle
    • Direction:方向
      • Left To Right:从左到右
      • Right To Left:从右到左
      • Bottom To Top:从下到上
      • Top To Bottom:从上到下
    • Min Value:最小值
    • Max Value:最大值
    • Whole Numbers:滑块代表的值只能是整数
    • Value:当前滑块所在的值(属性value:可读可写)
  • OnValueChanged(Single)
    • Single是单精度浮点型
    • 代码同toggle

6. Input Field 输入框

  • 由子物体Placeholder(Text)和Text(Text)组成
  • Input Field自带一个Image组件,代表输入框的背景
    • Placeholder:没有任何字符输出的时候,显示在输入框中
    • Text:用户输入的内容文本
  • Input Field(Script) 组件
    • Text Component:用户输入的文本显示在哪个文本框中,按本身的输入框文本样式的显示
    • Text:输入框中的文本
    • Character Limit:位数限制,0是不限位置
    • Content Type:文本类型
      • Standard:标准
        • Line Type:换行方式
          • Single Line:单行显示
          • Multi Line Submit:超出显示范围另起一行
          • Multi Line Newline:可以使用回车键另起一行
      • Autocorrected:自动纠错
      • Integer Number:只能输入整型数字
      • Decimal Number:可以输入浮点型数字
      • Alphanumeric:可以输入字母和数字
      • Name:每个单词的首字母自动大写
      • Email Address:可以输入邮件格式
      • Password:将输入的文字以星号*显示
      • Pin:将输入的文字以星号*显示,只能输入数字
      • Custom:自定义
        • Line Type:换行方式
          • Single Line
          • Multi Line Submit
          • Multi Line Newline
        • Input Type:输入方式
          • Standard
          • Auto Correct
          • Password
        • Keyboard Type:键盘模式
          • Default
          • ASCII Capable
          • Numbers And Punctuation
          • URL
          • Number Pad
          • Phone Pad
          • Name Phone Pad
          • Email Address
          • Nintendo Network Account
        • Character Validation:字符验证
          • None
          • Integer
          • Decimal
          • Alphanumeric
          • Name
          • Email Address
    • Caret Blink Rate:光标闪烁频率(1秒几次,默认0.85次)
    • Caret Width:光标宽度
    • Custom Caret Color:自定义光标颜色
    • Selection Color:自定义选择颜色
    • Hide Mobile Input:隐藏系统键盘,iOS专用
    • Read Only:只读不能写
  • On Value Changed(Stirng) 每次输入框改变都触发
  • On End Edit(String) 停止编辑后触发一次

7. Dropdown 下拉菜单

  • 由标签label 箭头arrow 模板template(下拉菜单每一个选项的模板)组成
  • Dropdown(Script)组件
    • Template
    • Caption Text:将选择的选项中的文字显示在指定的Text组件中
    • Caption Image:将选择的选项中的图片显示在指定的Image组件中
    • Item Text:每个选项中的文字显示在指定的Text组件中
    • Item Image:每个选项中可以加图片,显示在指定的Image组件中
    • Options
  • OnValueChanged(Int32)
    参数是Option的序号,从0开始
  • 代码设置Dropdown
// 声明一个dropdown
Dropdown _dropdown = GetComponent<Dropdown>();
// Dropdown.OptionData用来存Option数据
List<Dropdown.OptionData> data = new List<Dropdown.OptionData>()
{
    new Dropdown.OptionData(){ text = "helloworld1" },
    new Dropdown.OptionData(){ text = "helloworld2" }
};
// 两种方式设置options
// 1. 覆盖之前的所有设置
_dropdown.options = data;
// 2. 添加设置
_dropdown.AddOptions(List<Dropdown.OptionData> data);

8. Panel

  • 自带一个Image组件,是一个有勾边有圆角的Image,默认alpha通道小
  • 更多情况下来做容器,默认和父物体一样大

9. Rect Transform

继承自Transform,拥有transform的所有属性方法

  • Pivot:轴(心)点
    • 一个物体的中心,T工具中心的空心蓝点,
    • 如果选择了Center,不能移动轴点
    • 如果选择了Pivot,可以移动轴点
    • X:轴点的X比例,1是最右侧,0是最左侧
    • Y:轴点的Y比例,1是最上侧,0是最下侧
    • (0, 0)是左下角,(1, 1)是右上角
  • Anchor:锚点
    • 锚点是两个点
    • 锚点之间的关系
      • 锚点:Min = Max 是一个点
        • Pos X:轴点到锚点水平方向的间距,轴点 - 锚点
        • Pos Y:轴点到锚点垂直方向的间距,轴点 - 锚点
        • Width:物体的宽度
        • Height:物体的高度
      • 锚线:Min != Max 坐标有一个相同,有一个不同,两点连成一条线
        • 水平
          • Left:物体的左边缘到左边锚点的水平间距
          • Pos Y:轴点到锚点垂直方向的间距
          • Right:物体的右边缘到右边锚点的水平间距
          • Height:物体的高度
        • 垂直
          • Pos X:轴点到锚点水平方向的间距
          • Top:物体的上边缘到上边锚点的垂直间距
          • Width:物体的宽度
          • Bottom:物体的下边缘到下边锚点的垂直间距
      • 锚框:Min != Max坐标都不同,连城一个矩形,min在左下角,max在右上角
        • Left:物体的左边缘到左边锚点的水平间距
        • Right:物体的右边缘到右边锚点的水平间距
        • Top:物体的上边缘到上边锚点的垂直间距
        • Bottom:物体的下边缘到下边锚点的垂直间距
    • 锚点是相对于父物体的
  • 代码中修改RectTransform
// 获得RectTransform组件
RectTransform rect = GetComponent<RectTransfor>();

// 矩形右上角的相对于父物体的按比例坐标位置
rc.anchorMax = new Vector2(0.7f, 0.7f);
// 矩形左下角的相对于父物体的按比例坐标位置
rc.anchorMin = new Vector2(0.7f, 0.7f);

// 矩形的右上角相对于右上角锚点的偏移量(x轴,y轴)
// 两个参数是RectTransform的(-左下角,-右上角)
rc.offsetMax = new Vector2(10, 10);
// 矩形的左下角相对于左下角锚点的偏移量(x轴,y轴)
// 两个参数是RectTransform的(左上角,右下角)
rc.offsetMin = new Vector2(10, 10);

// 矩形的轴点相对于锚点的偏移量
rc.anchoredPosition = new Vector2(10, 10);

// 轴点的按比例坐标位置
rc.pivot = new Vector2(5, 5);

// 当两点重合时,代表高度和宽度
// 当两点不重合时(?跟失了智一样)
rc.sizeDelta = new Vector2(10, 10);

10. UGUI事件的回调

UGUI事件使用接口回调,使用时要使脚本类实现接口的方法

回调方法名 所在接口 说明
void OnPointerEnter(PointerEventData eventData) IPointerEnterHandler 鼠标进入时触发
void OnPointerExit(PointerEventData eventData) IPointerExitHandler 鼠标离开时触发
void OnPointerDown(PointerEventData eventData) IPointerDownHandler 鼠标按下时触发
void OnPointerUp(PointerEventData eventData) IPointerUpHandler 鼠标抬起时触发
void OnPointerClick(PointerEventData eventData) IPointerClickHandler 鼠标点击时触发
void OnBeginDrag(PointerEventData eventData) IBeginDragHandler 开始拖拽时触发
void OnDrag(PointerEventData eventData) IDragHandler 拖拽中持续触发
void OnEndDrag(PointerEventData eventData) IEndDragHandler 拖拽结束后触发

11. Canvas 画布

  • 添加UI组件时自动添加,所有的UI元素必须是canvas的子物体
  • 一个场景中可以放多个画布
  • 画布的大小:和屏幕一样大
  • Canvas组件
    • Render mode:渲染模式
      • Screen Space - Overlay:平铺,呈现在所有物体之前
        • Sort Order:决定多个画布的前后顺序,数字越大越靠前
        • Target Display:在指定通道渲染
      • Screen Space - Camera:指定摄像机渲染,物体前后顺序由Plane Distance决定。在UI上做些特效的时候使用。
        • Render Camera:指定使用哪个摄像机进行渲染
        • Plane Distance:画布距离摄像机的间距
        • Sorting Layers:在哪个图层排序
        • Order in Layer:在排序图层中的顺序,数字越大越靠前
      • World Space:可以把画布当做一个三维的物体来使用,此时Rect Transform可以修改
        • Event Camera:使用的摄像机
        • Sorting Layers:在哪个图层排序
        • Order in Layer:在排序图层中的顺序,数字越大越靠前
  • Canvas Scaler:多分辨率适配:找一个模板,按该模板的分辨率做UI,其他分辨率缩放
    • UI Scale Mode:屏幕缩放的模式
      • Constant Pixel Size:按恒定的像素大小缩放
      • Scale With Screen Size:按屏幕尺寸缩放
        • Reference Resolution:分辨率
        • Screen Match Mode:缩放模式
          • Match (Width ~ Height):按宽或高的比例缩放,或者是其中的某一个比例
      • Constant Physical Size:按恒定的物理尺寸缩放
  • Graphic Raycaster:决定能否相应事件
    按照默认设置即可

12. EventSystem 事件系统

添加UI组件时自动添加,表示这个场景中的所有画布能否响应事件

13. Camera 摄像机

  • 一个场景可以放多个摄像机
  • Camera组件
    • Clear Flags:摄像机背景,默认是Skybox
      • Skybox:天空盒
      • Solid Color:纯色背景
      • Depth Only:透明背景,显示物体不显示天空盒
      • Don't Clear:不清除物体原来的轨迹
    • Culling Mask:摄像机显示指定层的物体,默认Everything
      • Nothing
      • Everything
      • 各个层的名字...
    • Projection:摄像机的成像模式,默认Perspective透视图
      • Perspective:透视图,近大远小,视野范围是一个金砖型空间
        • Feild of View:视角,角度[0, 180],一般为60
      • Orthographic:正交图,物体的真实大小,视野范围就是一个立方体空间,可以做对齐操作
        • Size:摄像机的视野平面范围
    • Clipping Planes:摄像机视野的纵向范围,在近景面和远景面之间的物体可以被拍到
      • Near:近景面
      • Far:远景面
    • Viewpoint Rect:最终摄像机拍到的画面在屏幕中的位置和大小
      • 坐标点在左下角
      • XYWH四个值都是[0, 1]取值范围,代表比例
    • Depth:深度,控制摄像机的前后,数字越大越靠前,显示的越靠上
    • Target Texture:目标纹理图
  • 可添加组件
    • Skybox组件:使用指定的Skybox
  • 小地图的制作思路
    在Project中新建一个Render Texture(是一个纹理图,存在文件中),把Camera拍摄到的画面存到Render Texture中,在Hierarchy中新建一个Raw Image,用来展示Render Texture,此Raw Image就是小地图

14. Layout 布局组件

为了方便对某个物体的子物体布局位置和大小进行调整,布局组件互相之间冲突,最多只能有一个布局组件

  • Horizontal Layout Group:水平布局
    • Padding:内间距
      • Left
      • Right
      • Top
      • Bottom
    • Spacing:每两个物体之间的间距
    • Child Alignment:子物体的整体的对齐方式
    • Child Controls Size:能否通过布局组件修改子物体的宽高,勾选后子物体的宽高不能修改
    • Child Force Expand:通过调整物体位置,适当拉伸,填满整个屏幕
  • Vertical Layout Group:垂直布局,内容同水平布局
  • Grid Layout Group:网格布局
    • Padding:内间距
    • Cell Size:单元格大小
    • Spacing:间距,包含水平间距X和垂直间距Y
    • Start Corner:布局起始的位置和方向
    • Start Axis:布局轴,向哪个方向布局
    • Child Alignment:子物体的整体的对齐方式
    • Constraint:默认Flexible
      • Flexible:自由
      • Fixed Column Count:固定列数
      • Fixed Row Count:固定行数

15. Scroll View 滚动视图

由 ViewPort(展示区域) -> Content(展示内容) 和两个 ScrollBar(滚动条) -> Sliding Area(滑轨) -> Handle(滑块) 组成

  • Scroll View
    • Image组件:展示区域的背景
    • Scroll Rect(Script)组件
      • Content:关联能够显示的范围
      • Horizontal:是否支持在水平方向滚动,拖滚动条可以动,滚轮和拖内容不能动
      • Vertical:是否支持在垂直方向滚动
      • Movement Type:默认越界回弹
        • Unrestricted:不限制,越界之后原地停留
        • Elastic:越界回弹
          • Elasticity:弹性系数,值越大回弹越慢
        • Clamped:限制,不让越界
      • Inertia:是否有惯性
        • Deceleration Rate:减速率,0为没有惯性马上停,1为没有减速,1以上是加速
      • Scroll Sensitivity:滚动灵敏度,用滚轮滚动的时候的灵敏度
      • 关联子物体
      • Visibility:滚动条的显示与隐藏
      • On Value Changed(Vector2) 回调方法
  • Viewport:展示区域
    • Image组件
    • Mask组件:遮罩,遮住父物体外面的物体
  • Content:空物体,内容集合
    • 把想滚动的物体作为Content的子物体,Content的大小决定了能否滚动
    • 借助布局组件的方式
      • 任意添加一个布局组件
      • 添加一个组件Content Size Fitter:可以根据布局组件自动计算Content的大小
  • Scroll Bar:滚动条
    • Handele Rect:关联拖拽的滑块是哪一个
    • Value:滑块滑动表示一个值[0, 1]
    • Size:滑块的大小,表示一个比例[0, 1],如果是0,不会消失,以最小的形式出现
    • Number Of Steps:一个滑轨上有几个点可以让滑块出现,范围[0, 11],0的时候为平滑滑动
    • On Value Changed(Single)回调方法

六、动画

1. 一些知识

  • 模型中包含动画,由模型师来做,我们在获得的时候就决定了模型能做什么动画
  • Unity模型格式推荐.fbx
  • 模型在Inspector面板上类似Prefab
  • 模型子物体中包含各种零件、网格和动画

2. 旧版动画

  • Window -> Animation 创建动画(ctrl + 6 / Command + 6)
  • 需选中一个游戏对象
  • Samples:每秒播放多少帧
  • 横轴表示帧数
  • 编辑时选上录制按钮,改帧的Image和把图片拖到时间条上都可以
  • Add Property:添加动画物体需要修改属性
    • Rect Transform
    • Image
  • Doposheet:逐帧编辑
  • Curves:曲线图

3. 新版动画

  • Mecanim动画系统
    • 方便的实现任性动画的设置和重用
    • 方便的实现动画剪辑的设置
    • 可视化的动画控制界面
    • 对动画播放的精确控制
  • 模型及动画设置
    • Model 模型标签
      • Scale Factor:模型的缩放比例 Unity长度单位为米,一些模型软件长度单位是厘米,所以默认要设置0.01
    • Rig 动画配置标签
      • Animation Type
        • None:无动画
        • Legacy:旧版动画
        • Generic:通用动画(非人型动画)
        • Humanoid:新版人型动画
          • Avatar Definition:骨骼配置
          • Configure:配置,成功后前面会有个✔
            • 会打开新场景,能看到模型骨骼
            • Mapping:骨骼映射图
              • 实线圈:必须配置的骨骼,如果缺失,会影响整个动画的执行
              • 虚线圈:可选骨骼,配置会让动画更精细
              • 人是面对我们的,屏幕右边的是左臂,屏幕左边的是右臂
              • 三种方法做映射:屏幕上选中拖进去,Hierarchy中选中拖进去,拖到下面的列表中
              • Mapping -> Automap 自动映射
            • Muscles & Settings
              • Preview:预览
    • Animation 动画面板
      • Import Animation:导入动画,必选
      • Clips:动画片段
        • 截取片段:调整Start和End的帧数,横轴单位是由(秒:帧)表示的,帧率是30FPS
          • loop match:绿色没有问题,黄色红色都表示有问题
            • Loop Time:是否循环播放
            • Loop Pose:会结合最后一帧和第一帧做一个中和
          • Root Transform Rotation:动画旋转
            • Bake into Pose:动画朝向不会改变
          • Root Transform Position(X)
          • Root Transform Position(Y)
            • Bake into Pose:动画位移不会改变
          • Mirror:镜像动画

4. Animation组件

  • 用来播放旧版动画
  • Animation:默认在执行什么动画
  • Animations:集合,表示这个模型会什么动画,如果模型中包含A动画但是这个集合中没有A动画,则不能切换
// 声明动画组件
private Animation _animation;
// 获得组件
_animation = GetComponent<Animation>();
// 按键切换动画
if (Input.GetKeyDown(KeyCode.W))
{
    _animation.CrossFade("Walk");
}
else if (Input.GetKeyUp(KeyCode.W))
{
    _animation.CrossFade("Idle");
}

5. Animator 组件

  • Controller:动画状态机Runtime Animator Controller
  • Avatar:骨骼
  • Apply Root Motion:应用根动画,跟Bake into Pose联动

6. Animator Controller 动画状态机

滚轮缩放,alt加左键移动画面,把动画拖到状态机中松手,会自动新建状态,delete键删除联系

  • Inspector面板设置

    • Motion:执行的动画
    • Speed:动画播放速度
    • Mirror:镜像动画
    • Transitions:联系列表
      • Has Exit Time:有停止播放的时间,勾选上之后播放完前一个动画才会切换到下一个动画
      • Settings:过渡设置
        • Fixed Duration:自动过渡
        • Transition Duration:过渡时间
        • Transition Offset:过渡的偏移量
        • Interruption Source:打断源,动画可以被打断
      • Conditions:切换条件
        • 参数,大于或小于,具体数值
        • 多个条件的时候,是与的关系,满足了所有条件之后才会切换
  • Animator状态机设置

    • Layers:层
    • Parameters:参数,切换不同的状态
      • Float
      • Int
      • Bool
      • Trigger
    • 右键
      • Make Transition:建立联系,可以联系自己
      • Set as Layer Default State:设置为层默认状态
      • Copy
      • Create new BlendTree in State
      • Delete

    通过代码控制状态机:

    // 获取Animator组件
    private Animator _animator;
    _animator = GetComponent<Animator>();
    
    // 设置
    _animator.SetInteger(string name, int value);
    _animator.GetInteger(string name);
    _animator.SetFloat(string name, float value);
    _animator.GetFloat(string name);
    _animator.SetBool(string name, bool value);
    _animator.GetBool(stirng name);
    _animator.SetTrigger(string name);
    
    // 将一个字符串类型的参数名称转成一个对应的HashCode
    int Animator.StringToHash(string name);
    
    // 使用整型id设置,可以稍微提高效率
    _animator.SetInteger(int id, int value);
    _animator.GetInteger(int id);
    _animator.SetFloat(int id, float value);
    _animator.GetFloat(int id);
    _animator.SetBool(int id, bool value);
    _animator.GetBool(int id);
    _animator.SetTrigger(int id);
    
    
    • Add Behaviour 给状态添加行为,会生成一个脚本,里面包含StateMachineBehaviour回调
    // 参数 
    // animator:状态机所在的Animator组件,可以通过它.gameObject拿到游戏对象
    // stateInfo:动画状态信息,用来区分不同的状态
    // layerIndex:动画层下标,从0开始
    
    // 回调函数
    // 进入状态的时候调用一次
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {}
    // 在某个状态停留的时候持续调用,每帧一次
    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {}
    // 退出状态的时候调用一次
    override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {}
    // 会在OnAnimatorMove方法之前调用,如果使用了Apply Root Motion进行移动,就不要使用这个,unity会放弃自动位移
    override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {}
    // 调用IK动画方法OnAnimatorIK之后调用
    override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {}
    
    // 判断状态名字是否为指定的
    if (stateInfo.IsName("idle"))
    {
        Debug.Log("name idle");
    }
    // 判断状态的标签是否为指定的
    if (stateInfo.IsTag("idle"))
    {
        Debug.Log("tag idle");
    }
    

7. BlendTree 动画融合树

可以给几个动画做一个融合
右键 -> Create -> From new Blend Tree,双击编辑,点左上角导航条返回,右键Add motion添加动画
就是一种状态

  • Blend Type:融合类型
    • 1D:用一个参数来控制这些动画的切换
    • 2D:用两个参数来控制
      • Simple:所有被融合的动画方向不一致
      • Freedom Direction:相比第一个,需要有一个没有方向的动画
      • Freedom Cartesian:随意融合
      • Pos X:第一个参数
      • Pos Y:第二个参数
    • Direct:做表情动画
  • Thresholds:阈值
  • 播放倍数
  • 镜像动画
  • Automate Thresholds:是否自动设置阈值
  • Compute Thresholds:根据计算的值来设置阈值
    • speed:速率
    • Velocity X
    • Velocity Y
    • Velocity Z
    • AngularSpeed Deg
    • AngularSpeed Red

8. Layers 分层动画

每个层是有权重的

  • Base Layer:权重不能设置,默认为1
  • Avatar Mask:骨骼遮罩,添加遮罩后,该层上会有“M”标记
  • 层有下标,从0开始
  • 用代码修改层的权重
private Animator _animator;
_animator = GetComponent<Animator>();

if (Input.GetKeyDown(KeyCode.R)
{
    // 设置权重
    SetLayerWeight(int layerIndex, float value);
}

9. IK 动画

  • IK:反向动力学
  • 人型模型的IK部位:双手 双脚 眼睛
  • 动画状态机中的层设置,IK Pass按钮需要勾选,选上后该层上有“IK”标记
// 获取组件
private Animator _animator;
_animator = GetComponent<Animator>();
// 获取敌人
private Transform _enemy;
_enemy = GameObject.Find("enemy").transform;
// 如果要做IK动画,必须在这个方法中完成
private void OnAnimatorIK(int layerIndex)
{
    // 角度限制
    // 做一个角色指向敌人的向量
    Vector3 direction = _enemy.position - transform.position;
    // 求这个向量和角色前方的向量的夹角
    float angle = Vector3.Angle(direction, transform.forward);
    // 判断
    if (Mathf.Abs(angle)) <= 60)
    {
        // 设置IK部位的权重
        _animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1);
        // 设置IK部位的位置
        _animator.SetIKPosition(AvatarIKGoal.RightHand, _enemy.position);
    }
    
}

10. Curves 动画曲线

Animation标签中设置

  • 横轴:范围[0, 1],到1动画结束
  • 纵轴:随时间改变而变,代表一个值,而且我们能随时获取到
// 需求:根据动画中曲线Curve的值,来实时调整BoxCollider的高度
// 获取Animator组件
private Animator _animator;
_animator = GetComponent<Animator>();
// 获取BoxCollider
private BoxCollider _collider;
_collider = GetComponent<BoxCollider>();
// 获取Curve曲线的值
// 方式:在状态机中添加一个Float类型的参数,名字和曲线名字一致即可
float curve = _animator.GetFloat(Animator.StringToHash("Curve"));
_collider.size = new Vector3(_collider.size.x, curve, _collider.size.z);    // 这样会缩在人物中间
_collider.center = new Vector3(_collider.center.x, curve / 2, _collider.center.z);  // 再控制一下高度

11. Event 动画事件(帧事件)

在动画播放的过程中,让它去执行一个特定的方法,这个方法必须是在玩家身上的脚本中必须是public返回值是void,名字和设置的名字相同,参数可以是无参,如果需要参数,则参数类型必须是Float,Int,String,Object这四种之,不能写多个,值就是Inspector面板上写的值

public void Shout ()
{
    _audio.Play();
}

七、粒子特效

1. 创建

  • 右键 -> Effect -> Particle System
  • Scene面板中的Particle Effect
    • Play:播放特效
    • Restart:重新播放特效
    • Stop:停止播放特效
    • Playback Speed:播放的速度
    • Playback Time:已播放的时长(有速度加成)
    • Particles:粒子的数量
    • Speed Range:
    • Simulate layers:模拟的层
    • Resimulate:对粒子系统的更改是否马上生效
    • Show Bounds:显示边界

2. Particle System 组件

  • Particle System:粒子系统
    • Duration:粒子生成的持续时间
    • Looping:循环生成
    • Prewarm:预热,满数量开始特效
    • Start Delay:延迟生成
      下箭头展开
      • Constant:常量
      • Random Between Two Constants:在两个常量之间随机
    • Start Lifetime:粒子的生命周期,一个粒子从生成到销毁需要经历多长时间,默认5s
      下箭头展开
      • Constant:常量
      • Curve:曲线
        • 横轴:Duration 粒子生成的持续时间
        • 纵轴:常量
      • Random Between Two Constants:在两个常量之间取随机数
      • Random Between Two Curves:在两个曲线之间取随机数
    • Start Speed:粒子的初始移动速度
    • 3D Start Size:分轴调整粒子的初始大小
    • Start Size:粒子的初始大小
    • 3D Start Rotation:分轴设置粒子的初始旋转
    • Start Rotation:粒子的初始旋转
    • Flip Rotation:范围[0, 1],有这些比例的粒子会以反方向生成
    • Start Color:粒子的初始颜色
      • Color:纯色
      • Gradient:渐变
        上标签表示透明度,下标签表示颜色,最多加8个标签
        • Mode:调色板模式
          • Blend:渐变
          • Fixed:没有渐变直接改变颜色
        • Location:在时间轴的位置代表生命周期比例
      • Random Between Two Colors:在两种颜色重随机
      • Random Betwwen Two Gradients:在两个调色板中随机
      • Random Color:随机颜色
    • Gravity Modifier:重力调整
    • Simulation Space:坐标系,设置粒子特效会随哪个物体移动而移动
      • Local:本地坐标
      • World:世界坐标
      • Custom:自定义坐标
    • Simulation Speed:调整特效播放速度
    • Delta Time:是否使用Time.deltaTime属性来计算特效播放,用于在暂停的时候播放特效
    • Scaling mode:如何使用scale值
      • Hierarchy:从Hierarchy组合计算出自己的缩放值
      • Local:只使用自己的坐标系,忽略任何父物体
      • Shape:将缩放使用给粒子的位置,而不是缩放大小
    • Play On Awake:程序激活后就播放特效,会影响到同一个粒子系统中的其他特效
    • Emitter Velocity:当粒子系统移动的时候使用哪种方式计算速度
      • Transform:使用坐标计算速度
      • Rigidbody:使用刚体计算速度
    • Max Particles:在这个特效系统中的最大粒子数量
    • Auto Random Seed:是否每次激活时使用新的随机种子生成随机数
    • Stop Action:当粒子系统停止产生粒子并且所有的粒子都消失后,对游戏物体做什么动作
      • None:什么都不做
      • Disabled:取消激活
      • Destroy:销毁游戏物体
      • Callback:回调
  • Emission:生成
    • Rate Over Time:粒子每秒生成的数量(粒子生成速度)
    • Rate Over Distance:每移动单位长度能产生多少粒子,需要配合世界坐标系使用
    • Bursts:爆发产生粒子
      • Time:从特效播放开始多长时间开始Burst
      • Count:一次Burst产生多少数量的粒子
      • Cycles:这个特效产生多少次Burst
      • Interval:两次Burst之间的时间间隔
  • Shape:形状
    • Shape:
      • Sphere:球形
      • 半球形,上半球
      • Cone:锥形
      • Donut:环形
      • Box:立方体形
      • Mesh:网格
      • Circle:圆形
      • Line:线形
  • Velocity Over Lifetime:随生命周期修改粒子的速度
  • Force Over Lifetime:随生命周期修改粒子收到的力
  • Color Over Lifetime:随生命周期修改粒子的颜色
  • Size Over Lifetime:随声明周期修改粒子的大小
  • Renderer:渲染
    • 材质决定效果,标准资源包中有一些材质

3. Trail Renderer 轨迹渲染,拖尾

  • Trail Renderer 组件
    • Width:宽度,数值代表缩放比例

4. Line Renderer 线性渲染

  • Line Renderer 组件
    • Materials:材质
    • Loop:首尾相连
    • Width:宽度
    • Positions:位置
      • Size:位置集合的长度
      • Element 0:位置
  • 用代码控制线性渲染
// 获得组件
private LineRenderer _line;
_line = GetComponent<LineRenderer>();

// 设置线性渲染需要
// 1. 设置一共要经过多少个点
_line.positionCount = 3;
// 2. 设置每个点的坐标
// 通过每个点的序号设置每个点的坐标
_line.SetPosition(int index, Vecotr3 position); 
// 传一个向量数组作为所有点的坐标,导航的NavMeshPath返回的就是一个Vector3[],但是不包含起始点和终点
_line.SetPositions(Vector3[] positions);

// 设置loop,导航获得的路径不需要首尾相连
_line.loop = false;

八、导航

1. 导航的定义

  • 不同于现实中的GPS,unity中的导航主要体现在鼠标点击地面移动和自动寻路
  • 如果有多条路,会选择最近的路线进行导航
  • 导航物体需要放置在可移动的导航网格上的时候才可以开始导航

2. 步骤

  1. 设置导航静态物体(Navigation Static),哪些东西是不能动的
    方法如下:
    1. 在 Inspector 面板上 name 后面选择 Navigation Static
    2. 在 Navigation 面板上的 Object 中也能设置
    3. 可以设置父物体为导航静态物体,然后选择应用于子物体
  2. 烘焙地图
    但凡是场景发生改变,都需要重新烘焙地图
  3. 实现导航,添加 NavMeshAgent 组件
    脚本控制,脚本导航谁就挂给谁
// 使用命名空间
using UnityEngine.AI;
// 获取导航组件
private NavMeshAgent _agent;
_agent = GetComponent<NavMeshAgent>();
// 获取终点
private Transform _target;
_target = GameObject.Find("target").transform;
// 开启导航
if (Input.GetMouseButtonDown(0))
{
    // 想开启导航只需要设置终点即可
    // 设置属性
    _agent.destination = _target.position;
    // 使用方法
    _agent.SetDestination(_target.position);
}

3. Navigation 面板

  • window -> Navigation面板
    • Agents:可以设一些导航物体模型,给NavMeshAgent中的Agent Type用
    • Areas:分层
      • 系统自带三个层,也可以新建层,颜色对应地图上导航网格的颜色
        • Built-in 1:Not Walkable
        • Built-in 0:Walkable 默认为这个层
        • Built-in 2:Jump
        • 自定义的层...
      • Cost:Unity在计算线路时候附加的权重
    • Bake:烘焙地图
      • Baked Agent Size:根据多大的角色来烘焙地图
        导航物体是一个圆柱体
        • Agent Radius:圆柱体半径,不能设置太小,如果设置太小,Unity会提示;如果设为0,会被调为1
        • Agent Height:圆柱体高度
        • Max Slope:最大爬坡的坡度限制,范围[0, 60]
        • Step Height:台阶的高度,角色能迈上去的最高高度,最高和身高一样,超过的话按最高身高算
      • Generated Off Mesh Links:分离路面导航路线连接设置
        • Drop Height:允许向下掉落的最高高度
        • Jump Distance:允许跳跃的最大距离
      • Clear按钮,点击一次清除Scene面板中所有的导航网格
      • Bake按钮,点击一次获得蓝色导航网格,根据地图的复杂程度来决定烘焙时间,只能在导航面板才会看到导航网格,点击一次变成Cancel,不要连点
    • Object:更改指定物体的导航属性
      只有带Mesh Renderer的物体或Terrain才能被更改
      • Scene Filter:显示过滤器
        • All:所有物体
        • Mesh Renderers:所有带Mesh Renderer的物体
        • Terrain:所有地形
      • Navigation Static:导航静态物体
      • Generate OffMeshLinks:分离路面导航
      • Navigation Area:分层路面导航所在的层

4. NavMeshAgent组件

  • Agent Type:此物体的导航物体类型
  • Base Offset:导航圆柱体的物体本身的偏移量
  • Steering:导航速度相关设置
    • Speed:导航移动的最大速度
    • Angular Speed:导航移动的最大角速度
    • Acceleration:加速率
    • Stopping Distance:距离终点还有多远就停下来(例:去找npc,找到后会和npc保持一段距离,到达停止距离的时候开始减速,停的位置会比设置的距离近一些)
    • Auto Braking:自动刹车,当角色到导航终点的时候自动减速
  • Obstacle Avoidance:障碍物的回避设置
    • Radius:回避半径
    • Height:回避高度
    • Quality:回避质量,如果有大量的Agent,将这个值设为0,只处理碰撞,不处理其他的Agent和障碍,节省CPU性能
    • Priority:优先级,范围[0, 99],数字越低优先值越高,物体只会回避数字比自己低的障碍
  • Path Finding:寻路相关设置
    • Auto Traverse Off Mesh Link:自动通过OffMeshLink
    • Auto Repath:自动重新计算路线
    • Area Mask:物体能走哪些层

5. 代码控制

// 设置终点,自动开始导航
// 设置属性
_agent.destination = _target.position;
// 使用方法
_agent.SetDestination(_target.position);
    
// 能让导航圆柱体带动物体的位置更新
_agent.updatePosition = false;
_agent.updateRotation = false;

// 导航剩余距离
_agent.remainingDistance;   // 过大的时候显示Infinity

// 停止导航
_agent.Stop();  // 弃用
// 恢复导航
_agent.Resume();    // 弃用
// 建议使用
_agent.isStopped = true;    // true 为停 false 为导航

// 期望速度(Vector3),Scene面板上的红箭头
_agent.desiredVelosity
// 实际速度(Vector3),Scene面板上的蓝箭头
_agent.velosity

// 声明 path 储存路径信息
NavMeshPath path = new NavMeshPath();
// 计算能否导航到指定的点,如果能,把路径信息存到path中(返回值为bool)
_agent.CalculatePath(vector3 targetPosition, NavMeshPath path);
// 获取路径信息,信息包含了所有的拐点
Vector3[] corners = path.corners;

// 控制物体可以走哪些层(int)
// 0:Nothing
// -1:Everything
// 1:Walkable      1 << 0
// 2:Not Walkable  1 << 1
// 4: Jump          1 << 2
_agent.areaMask = 1 << 1 | 1 << 2;  // 可以走Not Walkable和Jump层

6. 动态路障

需要加Nav Mesh Obstacle组件,不用重新烘焙,不会影响导航网格,碰到路障后等待

  • Shape:形状
    • Capsule
    • Box
  • Center:中心点设置
    • X
    • Y
    • Z
  • Size:大小设置
    • X
    • Y
    • Z
  • Carve:碰到路障后是否重新规划路径,如果路都不通,就找一个距离终点最近的地方等待

7. 分离路面导航

  • 从一个路面到另一个路面上,两个路面没有联系
  • 一般情况下我们把除了地面之外的物体设为分离路面,Navigation面板上设置Off Mesh Link Generation下面的内容
  • 跳下去和跳过去的问题通过修改Navigation -> Bake -> Generate Off Mesh Links下面的值就可以,跳上去的问题需要用到Off Mesh Link组件
  • Off Mesh Link组件
    点到点跳跃时需要用到次组件
    • Start(Transform):起始点
    • End(Transform):终点
    • Cost Override:计算路径时候的权重
    • Bi Direction:是否为双向通行
    • Activated:是否激活
    • Auto Update Position:是否随着起点终点的改变而改变导航的线路
    • Navigation Area:导航线路所在的层

九、数据持久化存储

将数据存储在硬盘中

1. Player Prefs类

  • Unity内置的一个用来做数据存储的类
  • 优点:使用方便,简单
  • 缺点:存储的数据类型有限 int float string
  • 适用于:关卡数,最高分,UesrID等...
// 存
PlayerPrefs.SetInt(string key, int value);
PlayerPrefs.SetFloat(string key, float value);
PlayerPrefs.SetString(string key, string value);
// 取
PlayerPrefs.GetInt(string key);
PlayerPrefs.GetInt(string key, int defaultValue);   // 尝试取,如果能取到,返回值,如果取不到,返回默认值
PlayerPrefs.GetFloat(string key);
PlayerPrefs.GetFloat(string key, float defaultValue);   // 尝试取,如果能取到,返回值,如果取不到,返回默认值
PlayerPrefs.GetString(string key);
PlayerPrefs.GetString(string key, string defaultValue);   // 尝试取,如果能取到,返回值,如果取不到,返回默认值
  • 存储路径
    • Mac OS X:~/Library/Preferences/
    • Windows:HKCU\Software\ [company name]\ [product name]
  • 存储的内容不会互相转型,用什么类型存的就要用什么类型取
  • 存储的内容更新不及时,在程序结束的时候才会同步,需要及时Save()
// 删除:
// 删除指定键值对
PlayerPrefs.DeleteKey(string key);
// 删除所有的键值对
PlayerPrefs.DeleteAll();
// 判断文件中是否包含指定的键值对
bool has = PlayerPrefs.HasKey(string key);

// 将对文件的修改同步到文件
PlayerPrefs.Save();

2. XML

  • XML是一个特殊格式文档
  • XML的结构
    • 节点
      • 元素节点:成对出现 <x> </x>
      • 属性节点:包含name = "xiaoming"
      • 文本节点:元素节点包含的内容是文本节点
    • 元素节点是互相嵌套的,属性节点依赖于元素节点,文本节点包含在元素节点尖括号中
    • 根节点:直接写在文档中的元素节点,被称为是根节点(ROOT),在一个XML文档中,根节点有且只有一个
<!-- 头部信息 -->
<?xml verson="1.0" encoding="utf-8"?>
<Person>
    <name type="string" type1="hello world">xiaoming</name>
    <age>10</age>
    <Pet>
        <Dog>
            <name>hashiqi</name>
            <age>1</age>
        </Dog>
    </Pet>
    <Pets>
        <Dog>
            <name>hashiqi</name>
            <age>2</age>
        </Dog>
        <Dog>
            <name>taidi</name>
            <age>3</age>
        </Dog>
        <Dog>
            <name>keji</name>
            <age></age>
            <!-- 如果节点没有任何内容和子节点,可以直接内部闭合 -->
            <age />
        </Dog>
    </Pets>
</Person>
  • C# 中的 System.Xml 类

    • 继承结构:

      • System.Object
        • System.Xml.XmlNode(表示XML节点)
          • System.Xml.XmlDocument(表示XML文档)
          • System.Xml.XmlAttribute(表示XML属性节点)
          • System.Xml.XmlLinkedNode
            • System.Xml.XmlElement(表示XMl元素节点)
    • 属性和方法:

      • System.Xml.XmlNode:所有节点的父类
      属性和方法 作用
      InnerText 获取或设置文本节点的值
      InnerXml 获取 或 通过直接编写XML设置子节点
      Value 获取或设置属性节点的值
      AppendChild(XmlNode node) 将制定的节点添加到该节点的子节点列表的末尾
      XmlNodeList ChildNodes 获取所有子节点
      XmlNode FirstChild 获取第一个子节点
      bool HasChildNode 判断该节点是否包含任何子节点
      SelectSingleNode(string XPath) 选择匹配XPath表达式的第一个节点
      SelectNodes(string XPath) 选择所有匹配XPath表达式的节点
      • System.Xml.XmlDocument:文档
      属性和方法 作用
      CreateXmlDeclaration(string version, string encoding, string standalone) 创建一个具有指定参数的描述(头节点)
      CreateElement(string name) 创建具有指定名称的元素节点
      CreateNode 创建任意一种节点,使用时需要向下转型
      AppendChild(XmlNode node) 继承自XmlNode,添加子节点
      Save(string path) 将XML文档保存到指定的文件
      Load(string filename) +3 overload 从指定的URL加载XML文档
      LoadXML(string xml) 从指定的字符串加载XML文档
      • XmlElement:元素节点
      属性和方法 作用
      SetAttribute(string attributeName, string attributeValue) 设置具有指定名称的属性节点
      GetAttribute(string attributeName) 返回具有指定名称的属性值
      GetAttributeNode(string name) +1overload 获得具有指定名称的属性节点
      HasAttributes(string name) 判断该元素节点是否包含属性
  • 生成XML的步骤

    1. 引用命名空间 System.Xml
    2. 生成XML文档(XmlDocument类)
    3. 生成根元素(XmlElement类)添加给文档对象
    4. 依次生成子元素添加给父元素
    5. 将生成的XML文档保存
  • 使用代码生成XML文档

// 1. 引用命名空间
using System.Xml;

// 2.1 设定文档路径
private string path = Application.dataPath + "/File/boxCollider.xml";
// 2.2 创建XML文档
// 2.2.1 创建一个XmlDocument文档对象
XmlDocument document = new XmlDocument();
// 2.2.2 创建一个头部描述文件,version需要是一个可以转型成float类的值,standalone传null
XmlDeclaration declaration = document.CreateXmlDeclaration("1.0", "utf-8", null);
// 2.2.3 将生成的描述信息添加到文档中
document.AppendChild(declaration);

// 3.1 创建一个根元素节点
XmlElement root = document.CreateElement("BoxCollider");
// 3.2 将这个节点添加到文档中
document.AppendChild(root);

// 4.1.1 创建元素节点
XmlElement trigger = document.CreateElement("isTrigger");
// 4.1.2 给root添加子节点
root.AppendChild(trigger);
// 4.1.3 给元素节点设置文本节点
trigger.InnerText = "False";

// 4.1.1 创建元素节点
XmlElement material = document.CreateElement("material");
// 4.1.2 给root添加子节点
root.AppendChild(material);
// 4.1.3 给元素节点设置文本节点
trigger.InnerText = "None";

// 5 添加属性节点
// 5.1.1 创建一个属性节点
XmlAttribute attribute1 = document.CreateAttribute("type");
// 5.1.2 设置属性节点的值
attribute1.Value = "Physic Material";
// 5.1.3 给元素节点添加属性节点
material.setAttributeNode(attribute1);

// 5.2 设置属性节点的方法
material.SetAttribute("type", "Physic Material");

// 6. 删除节点
trigger.RemoveAll();
root.RemoveChild(material);

// 7. 直接设置内部xml元素
root.InnerXml = "<size><x>0</x><y>0</y><z>0</z></size>";

// 8. 保存xml文件
document.Save(path); // 需要传入完整的路径加文件名,如果路径文件存在,则会覆盖原文件
  • 使用代码读取XML文档
// 创建一个XmlDocument对象
XmlDocument document = new XmlDocument();

// 2. 用document读取内容
// 读取指定的文档
document.load(path);
// 读取指定的xml字符串
document.loadXml(@"
<size>
    <x>1</x>
    <y>1</y>
    <z>1</z>
</size>
")

// 1. 获取所有的子节点
XmlNodeList list = document.ChildNodes;

// 2. 获取第一个子节点
XmlNode root = document.FirstChild;

// 3. 获取指定的节点
XmlNode node = document.SelectSingleNode("Inspector");

// 4. 获取一个节点的InnerText
XmlNode node = document.SelectSingleNode("//BoxCollider");
string innerText = node.InnerText;

// 5. 获取一个节点的属性
XmlNode node = document.SelectSingleNode("/Inspector/BoxCollider/Material");      
// 将node强转为Element
XmlElement element = node as XmlElement;
// 获取element身上的属性节点
XmlAttribute attr = element.GetAttributeNode("type");

// 5.2 获取属性
string result = element.GetAttribute("type");
  • 常用的XPath语法
// 1. / 路径分隔符
XmlNode node = document.SelectSingleNode("/Inspector/GameObject");

// 2. // 查找节点, 无论这个节点在哪都能找到
XmlNode trigger = node.SelectSingleNode("//IsTrigger");

// 3. .当前目录  ..父目录
XmlNode node = document.SelectSingleNode("//Center");
XmlNode x = node.SelectSingleNode("../IsTrigger");

// 4. 查找指定下标的节点
// 查询第2个x
XmlNode node = document.SelectSingleNode("/Inspector/x[2]");
// 查询最后一个x
XmlNode node = document.SelectSingleNode("/Inspector/x[last()]");
// 查询倒数第二个
XmlNode node = document.SelectSingleNode("/Inspector/x[last() - 1]");
// 查询序号大于1小于4的x
XmlNodeList list = document.SelectNodes("/Inspector/x[position() < 4 and position() > 1]");

// 5. |
// 查找所有的x和y节点
XmlNodeList list = document.SelectNodes("//x | //y");

// 6. 查找Pet中第二个Dog元素的name元素
XmlNode node = document.SelectSingleNode("//Pet/Dog[2]/name");

// 7. 查找指定的Person元素,要求Person中的age元素必须大于30
XmlNodeList list = document.SelectNodes("/Inspector/Person[age>30]");

// 8. 查找具有指定属性type的material节点
XmlNodeList list = document.SelectNodes("//Material[@type]");

// 9. 查找指定的属性type
XmlNodeList list = document.SelectNodes("//@type");

3. JSON

  • 语法:
    • 大括号:写键值对,键和值之间用冒号分隔,键值对之间用逗号分隔,类似字典
    • 中括号:写数据,类似数组
// json的注释
/*
json的注释
*/

{
    "name": "xiaoming",
    "age": 10,
    "gender": '男'
    "pet":
    {
        "name": "hashiqi",
        "age": 3
    }
}
// 另一段json
{
    "name": "诺克萨斯之手",
    "hp": 582.24,
    "attack": 55.88,
    "skills":
    [{
        "name": "被动技能-出血"
    },{
        "name": "Q技能-大杀四方",
        "cd": [9, 8, 7, 6, 5],
        "mp": [30, 30, 30, 30, 30]
    },{
        "name": "W技能-致残打击",
        "cd": [9, 8, 7, 6, 5],
        "mp": [30, 30, 30, 30, 30]
    },{
        "name": "E技能-无情铁手",
        "cd": [24, 21, 18, 15, 12]
    },{
        "name": "R技能-诺克萨斯断头台",
        "cd": [120, 100, 80],
        "mp": [100, 100, 0]
    }]
}
  • Unity使用的是 .net 2.0,没有对Json的支持,需要用第三方库
    • System.Json(便于Json生成)
    • LitJson(便于Json解析)
  • System.Json 中的类
    • JsonArray:数组
    • JsonObject:键值对
    • JsonValue:具体的值,可以是一个数字,一个字符串等,也可以是一个JsonArray或JsonObject
  • LitJson
    • 对象转Json:JsonMapper.ToJson(T model)
    • Json转对象:JsonMapper.ToObject<T>(string jsonString)
    • Project文件夹中建立Plugins文件夹,用于存放第三方库
      Model.cs
public class Player
{
    public string name;
    public int age;
    public char gender;
    public Dog pet;
}

public class Dog
{
    public string name;
    public int age;
    public char gender;
}

JSONOperation.cs

using LitJSON;

// 实例化一个对象
Player player = new Player();
// 输入对象的信息
player.name = "xiaoming";
...

// 将Player的信息生成一个json
string json = JsonMapper.ToJson(player);
PlayerPrefs.SetString("json", json);

// 根据一个Json字符串解析成模型
string json = PlayerPrefs.GetString("json");
Player player = JsonMapper.ToObject<Player>(json);
  • JsonUtility 类
    • Unity集成的用来解析生成json的类,用法和LitJson一模一样,效率完胜LitJson
    • 模型中嵌套的类需要添加特性[System.Serializable]
      Model.cs
public class Player
{
    public string name;
    public int age;
    public char gender;
    public Dog pet;
}

[System.Serializable]
public class Dog
{
    public string name;
    public int age;
    public char gender;
}

JsonUtility.cs

// 模型转Json
string json = JsonUtility.ToJson(player);
// Json转模型
Player player = JsonUtility.FromJson<Player>(json);
  • JsonUtility 中无法存 List<T> 和 Dictionary<Tkey, Tvalue> 的解决办法:

    • List<T>:使用一个类来包含List<T>
      public class PersonModel : ISerializationCallbackReceiver
      {
          public List<Person> person;
      
          public PersonModel()
          {
              person = new List<Person>();
          }
      }
      
      [Serializable]
      public class Person
      {
          public string name;
          public int age;
          public Dog[] dogs;
      
          public Person()
          {
              dogs = new Dog[2];
          }
      }
      
      [Serializable]
      public class Dog
      {
          public string name;
          public int age;
          public Gender gender;
      }
      
      public enum Gender
      {
          male, female
      }
      
    • Dictionary<Tkey, Tvalue>:使用接口ISerializationCallbackReceiver
      public class PersonModel : ISerializationCallbackReceiver
      {
          [SerializeField]
          List<int> keys;
          [SerializeField]
          List<Person> values;
      
          public Dictionary<int, Person> personList;
      
          public PersonModel()
          {
              personList = new Dictionary<int, Person>();
          }
      
          // 接口的实现方法,解序列化
          public void OnAfterDeserialize()
          {
              var count = Math.Min(keys.Count, values.Count);
              personList = new Dictionary<int, Person>(count);
              for (var i = 0; i < count; ++i)
              {
                  personList.Add(keys[i], values[i]);
              }
          }
      
          // 接口的实现方法,序列化
          public void OnBeforeSerialize()
          {
              keys = new List<int>(personList.Keys);
              values = new List<Person>(personList.Values);
          }
      }
      
  • JsonObject插件

    • JsonUtility转对象的时候,如果对象包含子类,是无法转换到子类的,这时候需要使用JsonObject
    • 在AssetStore下载JsonObject插件,并导入到Unity中
    • 使用代码解析Json字符串,代码实例:
    private List<Item> itemList = new List<Item>();
     
    /// <summary>
    /// 解析物品Json
    /// </summary>
    public void ParseItemJson(string itemsJson)
    {
        JSONObject j = new JSONObject(itemsJson);
    
        foreach (JSONObject temp in j.list)
        {
            int id = (int)temp["id"].n;
            string name = temp["name"].str;
            Item.ItemType type = (Item.ItemType)System.Enum.Parse(typeof(Item.ItemType), temp["type"].str);
    
            Item item = null;
            switch (type)
            {
                case Item.ItemType.Consumable:
                    int hp = (int)temp["hp"].n;
                    int mp = (int)temp["mp"].n;
                    item = new Consumable(id, name, type, hp, mp);
                    break;
                // 其他类型省略
                default:
                    break;
            }
    
            Debug.Log("item.id = " + item.ID + " , consumable.hp = " + ((Consumable)item).HP);
            itemList.Add(item);
        }
    }
    

4. 数据库

4.1 基础知识

  • 数据库分类
    • 本地数据库
      • Sqlite 轻便
    • 网络数据库
      • Oracle
      • SQL Server
      • MySQL
      • DB2
  • SqliteManager 可视化管理工具
    • Unity支持的数据库后缀名 .sqlite .db
  • 数据库中数据的存储格式
    • 表格
      • 字段(键),不会重复的键称为主键,主键最多1个,可以没有

4.2 SQL语句

操作 语句 备注
建表 CREATE TABLE 表名 (键 类型, 键 类型, ...);
建表改进 CREATE TABLE IF NOT EXISTS 表名 (键 类型, 键 类型, ...); 只有不存在此表的时候才添加,避免报异常
给表格中所有的字段赋值 INSERT INTO 表名 VALUES (值,值,值,值); 有多少键写多少值
给部分字段赋值 INSERT INTO 表名 (键,键,键,...) VALUES (值,值,值,...); 键的顺序可以不和建表的时候一样,如果表中有NOT NULL的字段但是没有赋值,会报错
条件语句 WHERE 条件 > >= < <= = != AND OR NOT/! + - * / %
从表中删除所有的数据 DELETE FROM 表名;
从表中删除满足条件的数据 DELETE FROM 表名 WHERE 条件;
从表中修改全部的数据 UPDATE 表名 SET 键=值, 键=值, ...;
从表中修改满足条件的数据 UPDATE 表名 SET 键=值, 键=值, ... WHERE 条件;
查询表中所有的数据的所有字段值 SELECT * FROM 表名;
查询所有满足条件的数据的所有字段值 SELECT * FROM 表名 WHERE 条件;
查询所有满足条件的数据的指定字段值 SELECT 字段, 字段, ... FROM 表名; 按查询时字段的顺序显示
查询结果排序 SELECT * FROM 表名 WHERE 条件 ORDER BY 键 (ASC/DESC), 键 (ASC/DESC), 键 (ASC/DESC), ...; ASC 升序 DESC 降序,默认按照主键升序,默认升序所以ASC可以省略
  • SQL语句不区分大小写,表名和键是区分大小写的
  • 每一个键都可以有修饰,修饰语句放到类型后面
    常用修饰符:
    • PRIMARY KEY:主键
    • NOT NULL:非空类型
  • 类型可以不写,默认是TEXT类型
// 需求:Person
// 字段:姓名(主键),年龄,性别

// 建表
CREATE TABLE Person (name TEXT PRIMARY KEY NOT NULL, age INTEGER, gender);
CREATE TABLE IF NOT EXISTS Person (name TEXT PRIMARY KEY NOT NULL, age INTEGER, gender);
// 增
INSERT INTO Person VALUES ('小明', 10, '男');
INSERT INTO Person (name, age) VALUES ('小红', 12);
// 删
DELETE FROM Person WHERE age > 100 AND age < 20000;
DELETE FROM Person WHERE age % 2 = 1;
// 改
UPDATE Person SET gender = '不知道';
UPDATE Person SET gender = '知道' WHERE age > 100;
// 查
SELECT * FROM Person WHERE age > 100 ORDER BY gender DESC;

4.3 代码使用SQL语句

  • 需要用到的动态链接库
    • Sqlite3.dll
    • Mono.Data.Sqlite.dll
    • System.Data.dll
      • Window:\Unity\Editor\Data\Mono\lib\mono\2.0\
      • MacOS:/Application/Unity/Unity.app/contents/Mono/lib/mono/2.0
  • 使用到的类
描述
SqliteConnection 与数据库的连接对象
SqliteCommand 执行SQL语句的对象
SqliteDataReader 读取数据的对象
  • SqliteConnection的方法
方法 作用
SqliteConnection(path); 构造方法,若路径下有数据库文件则建立连接,若没有数据库则新建数据库文件
SqliteCommand CreateCommand(); 创建操作指令对象
Open(); 打开数据库
Close(); 关闭数据库
  • SqliteCommand的方法
方法 作用
int ExecuteNonQuery(); 执行一个非查询语句,返回有多少行数据收到影响
object ExecuteScalar(); 执行一个查询语句,返回查询到的数据中的第一行第一列
SqliteDataReader ExecuteReader(); 执行一个查询语句,返回一个SqliteDataReader
  • SqliteDataReader的属性与方法
    使用SqliteDataReader之后要关闭它,否则被视为上次的查询操作还没完成,无法再设置CommandText
属性与方法 作用
bool Read(); 类似于索引器的MoveNext(),读取下一个,返回是否有数据
FieldCount 键的数量
string GetName(int index); 通过键的下标,获取键的名称
object GetValue(int index); 通过键的下标,获取键的值
Close(); SqliteDataReader的方法,关闭SqliteDataReader
using Mono.Data.Sqlite;

// 类
// 与数据库的连接对象
SqliteConnection _connection;
// 执行SQL语句的对象
SqliteCommand _command;
// 读取数据的对象
SqliteDataReader _reader;

// 1. 建立与数据库的连接
// 如果路径下没有数据库,系统会自动帮我们新建一个数据库,如果路径下有数据库,则建立连接
// 1.1 拼接一个数据库的路径
string path = "Data Source = " + Application.dataPath + "/Database/myDatabase.sqlite";
// 1.2 建立与数据库的连接
_connection = new SqliteConnection(path);
// 1.3 创建操作指令对象
_command = _connection.CreateCommand();
// 1.4 打开数据库
// 有打开数据库,就需要有关闭数据库,如果程序结束后数据库没关,数据库会锁上自己,再解锁很麻烦
_connection.Open();

// 2. 建表
// 2.1 写SQL语句
string sql = @"CREATE TABLE IF NOT EXISTS Hero (name TEXT PRIMARY KEY, gender TEXT, book TEXT);";
// 2.2 设置操作指令
_command.CommandText = sql;
// 2.3 执行SQL操作
// 执行一个非查询的语句
_command.ExecuteNonQuery();

// 3. 添加数据
// 3.1 添加一行数据
string sql = @"INSERT INTO Hero VALUES ("萧炎", "男", "斗破苍穹");";
_command.CommandText = sql;
_command.ExecuteNonQuery();

// 3.2 添加多行数据,并返回这个语句对多少行数据产生影响
string[] names = { "林动", "牧尘", "叶凡", "石昊", "唐三", "秦羽" };
string[] genders = {"男", "男", "男", "男", "女", "女"};
string[] books = { "武动乾坤", "大主宰", "遮天", "完美世界", "斗罗大陆", "星辰变" };
// SQL语句
string sql = "";
// 循环拼接
for (int i = 0; i < names.Length; i++)
{
    sql += string.Format("insert into Hero values ('{0}', '{1}', '{2}');", names[i], genders[i], books[i]);
}
_command.CommandText = sql;
// 执行非查询语句并记录返回值
int result = _command.ExecuteNonQuery();
Debug.Log(result);

// 4. 删除数据
_command.CommandText = @"delete from Hero where gender = '女';";
int result = _command.ExecuteNonQuery();
Debug.Log("删除了" + result + "行数据");

// 5. 修改数据
_command.CommandText = @"update Hero set gender = '不知道' where name = '牧尘';";
_command.ExecuteNonQuery();

// 6. 查询数据
// 6.1 返回查询到的数据中的第一行第一列
_command.CommandText = @"select * from Hero;";
object result = _command.ExecuteScalar();
Debug.Log(result);

// 6.2 使用_reader来遍历获取所有的查询结果
_command.CommandText = @"select * from Hero;";
_reader = _command.ExecuteReader();

while (_reader.Read())
{
    string s = "";
    // 6.2.1 通过键的名字来取
    s += _reader["name"] + " | ";
    s += _reader["gender"] + " | ";
    s += _reader["book"];         
    Debug.Log(s);

    // 6.2.2 通过键的下标来取
    s += _reader[0] + " | ";
    s += _reader[1] + " | ";
    s += _reader[2];
    Debug.Log(s);

    // 6.2.3 循环遍历
    for (int i = 0; i < _reader.FieldCount; i++)
    {
        // 通过键下标, 获取键的名称
        string key = _reader.GetName(i);
        // 通过键的下标, 获取键的值
        object value = _reader.GetValue(i);
        s += _reader[i] + " | ";
    }
    Debug.Log(s);
}

// 注意!: _reader在使用结束后, 记得要关闭
_reader.Close();

// 7. 关闭数据库
private void OnDestroy()
{
    _connection.Close();
}

十、进程,线程与协程

1. 进程

  • 程序的执行单元,是程序执行所需要所有资源的总和,一个进程就相当于是一个程序,一个程序可以起多个进程,解决了多任务并发的问题
  • PC端多用进程,移动端多用线程

2. 线程

  • 程序执行的最小单元,进程包含线程,进程创建的时候会自动创建一个进程,称为主线程
  • 一个进程可以包含多个线程,线程之间也有层级关系,A线程开辟了B线程,当A线程结束的时候,B线程也结束了
  • 理论上说,线程越多,程序执行的效率越高,但是如果无限开辟线程反而会使程序执行效率降低,因为开辟线程需要消耗资源,同时线程会去分掉CPU的时间片
  • 线程之间可以资源共享,而进程不能,为了避免多个线程同时访问一个资源,使用了线程锁,保证了同时只有一个线程访问资源
  • 线程对游戏引擎不友好,游戏中需要执行的任务几乎都需要用主程序来执行

3. 协程(协同程序)

  • 协程不是多线程,是运行在主线程中的一个程序
  • 协程每帧执行一次,顺序是Update() -> coroutine -> LateUpdate()
// 动态返回一个枚举器接口的实现类
// 每调用一次yield return,将i加入到一个可枚举的序列里
public IEnumerator GetNumber()
{
    for (int i = 0; i < 10; i++)
    {
        // 输出i
        Console.WriteLine(i);
        // 动态返回
        yield return 0;
    }
}

public IEnumerator GetNumber2()
{
    for (int i = 0; i < 10; i++)
    {
        // 输出i
        Console.WriteLine(i);
        // 每3秒执行一次
        yield return new WaitForSeconds(3);
    }
}

// 协程的嵌套
public IEnumerator GetNumber3()
{
    for (int i = 0; i < 10; i++)
    {
        // 输出i
        Console.WriteLine(i);
        // 等待下面的协程执行结束,继续执行
        yield return StartCoroutine(GetNumber4());
        // 是下面这种写法的简写
        // Coroutine t = GetNumber4();
        // yield return t;
    }
}
public IEnumerator GetNumber4()
{
    for (int i = 0; i < 10; i++)
    {
        // 输出i
        Console.WriteLine(i);
        yield return 0;
    }
}

  • 开启协程
// 用IEnumerator开启协程
StartCoroutine(IEnumerator enumerator);
// 使用方法名开启协程,返回值一定是IEnumerator
StartCoroutine(string MethodName);
StartCoroutine(stirng MethodName, object value);
  • 停止协程
Coroutine t = StartCoroutine("GetNumber2");
StopCorountine(t);
// 停止全部的协程
StopAllCoroutine();
// 关键字跳出协程
yield break;
  • WWW类
    数据下载的类,使用它在网络中下载资源,一定要放在协程里面做下载
void Start()
{
    string path = "http://...想下载图片的url";
    StartCoroutine(Download(path));
}

private IEnumerator Download(string path)
{
    WWW www = new WWW(path);
    
    while(www.isDone == false)
    {
        // 下载中,打印进度
        Debug.Log(www.progress);
        yield return 0;
    }
    
    // 跳出循环,说明下载完成
    
}

private IEnumerator Download2(string path)
{
    WWW www = new WWW(path);
    // 等待下载
    yield return www;
    // 下载完成
}

十一、开发时遇到的一些问题

1. 鼠标点击穿透UI

  • EventSystem
    EventSystem中有方法IsPointerOverGameObject()用于判定是否鼠标划入了UI游戏物体
void Update()
{
    // 判断鼠标是否在UI游戏物体上,如果在,就return
    if(EventSystem.current.IsPointerOverGameObject())   return;
    // 处理点击逻辑
    // ...
}
  • 利用UI的GraphicRaycaster判定

3D场景中Canvas和场景不是在一个平面上,所以在做鼠标点击事件的时候射线不会被UI挡住,在操作UI的时候背后的场景内也会与点击互动,解决办法如下:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystem;

public class UIInterceptRaycast : MonoBehaviour 
{
    // EventSystem
    private EventSystem _eventSystem;
    // GraphicRaycaster
    private GraphicRaycaster _caster;

    private void Awake()
    {
        _eventSystem = GameObject.Find("EventSystem").GetComponent<EventSystem>();
        _caster = GameObject.Find("Canvas").GetComponent<GraphicRaycaster>();
    }

    private void Update()
    {
        if (CheckGuiRaycastObjects()) return;
        
        // 在这里处理点击事件
        // ...
    }

    // 检查射线是否打中UI上的物体
    bool CheckGuiRaycastObjects()
    {
        PointerEventData eventData = new PointerEventData(_eventSystem)
        {
            pressPosition = Input.mousePosition,
            position = Input.mousePosition
        };

        List<RaycastResult> list = new List<RaycastResult>();
        _caster.Raycast(eventData, list);
        //Debug.Log(list.Count);
        return list.Count > 0;
    }
}

注意:在做UI的时候一般会用一个Panel做根目录,这个panel也会被添加到GraphicRegistry中的公共列表中,如果是这样的话记得把list.Count > 0改成list.Count > 1,或者直接删除Panel上的继承自Graphic的组件

2. A*算法二维世界寻路

算法核心是存在两个集合,Open和Close,Open集合用于存可以移动的点,Close集合用于存舍弃的点。然后计算Open集合中每个点的F = G + H,G是从起点出发到当前点所消耗的数值,H是当前点到终点的预计消耗,取F最小的点为下一个移动的点,将其从Open集合中删除,加入到Close集合,循环执行,直到Open集合中没有元素,则找到了end

  • Point.cs 用于存放点的信息的模型
public class Point 
{
    public Point Parent { get; set; }
    public float H;
    public int X;
    public int Y;
    public float G;
    public float F;
    public bool IsWall;
    public Point(int x,int y,Point parent=null)
    {
        this.X = x;
        this.Y = y;
        this.Parent = parent;
    }
    public void UpdateParent(Point parent,float g)
    {
        this.Parent = parent;
        this.G = g;
        this.F = G+H;
    }
}
  • AStar.cs 寻路算法
using System.Collections.Generic;
using UnityEngine;

public class AStar : MonoBehaviour
{
    private const int mapWidth = 15;
    private const int mapHeight = 15;

    private Point[,] map = new Point[mapWidth, mapHeight];

    // Use this for initialization
    void Start()
    {
        InitMap();  //初始化地图
        Point start = map[2, 2];
        Point end = map[12, 12];
        FindPath(start, end);
        ShowPath(start, end);
    }

    private void ShowPath(Point start, Point end)
    {
        int z = -1;
        Point temp = end;
        while (true)
        {
            //Debug.Log(temp.X + "," + temp.Y);
            Color c = Color.gray;
            if (temp == start)
            {
                c = Color.green;
            }
            else if (temp == end)
            {
                c = Color.red;
            }
            CreateCube(temp.X, temp.Y, z, c);

            if (temp.Parent == null)
                break;
            temp = temp.Parent;
        }
        for (int x = 0; x < mapWidth; x++)
        {
            for (int y = 0; y < mapHeight; y++)
            {
                if (map[x, y].IsWall)
                {
                    CreateCube(x, y, z, Color.blue);
                }
            }
        }
    }

    private void CreateCube(int x, int y, int z, Color color)
    {
        GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube);
        go.name = x + "," + y;
        go.transform.position = new Vector3(x, y, z);
        go.GetComponent<Renderer>().material.color = color;
    }

    private void InitMap()
    {
        for (int x = 0; x < mapWidth; x++)
        {
            for (int y = 0; y < mapHeight; y++)
            {
                map[x, y] = new Point(x, y);
                CreateCube(x, y, 0, Color.black);
            }
        }
        map[4, 1].IsWall = true;
        map[4, 2].IsWall = true;
        map[4, 3].IsWall = true;
        map[4, 4].IsWall = true;
        map[4, 5].IsWall = true;
        map[4, 6].IsWall = true;
    }

    /// <summary>
    /// 查找最优路径
    /// </summary>
    /// <param name="start"></param>
    /// <param name="end"></param>
    private void FindPath(Point start, Point end)
    {
        List<Point> openList = new List<Point>();
        List<Point> closeList = new List<Point>();
        openList.Add(start);    //将开始位置添加进Open列表
        while (openList.Count > 0)//查找退出条件
        {
            Point point = FindMinFOfPoint(openList);//查找Open列表中最小的f值
            //print(point.F + ";" + point.X + "," + point.Y);
            openList.Remove(point); closeList.Add(point);//不再考虑当前节点

            List<Point> surroundPoints = GetSurroundPoints(point);//得到当前节点的四周8个节点
            PointsFilter(surroundPoints, closeList);//将周围节点中已经添加进Close列表中的节点移除
            foreach (Point surroundPoint in surroundPoints)
            {
                if (openList.IndexOf(surroundPoint) > -1)//如果周围节点在open列表中
                {
                    float nowG = CalcG(surroundPoint, surroundPoint.Parent);//计算经过的Open列表中最小f值到周围节点的G值
                    if (nowG < surroundPoint.G)
                    {
                        print("123");
                        surroundPoint.UpdateParent(point, nowG);
                    }
                }
                else//周围节点不在Open列表中
                {
                    surroundPoint.Parent = point;//设置周围列表的父节点
                    CalcF(surroundPoint, end);//计算周围节点的F,G,H值
                    openList.Add(surroundPoint);//最后将周围节点添加进Open列表
                }
            }
            //判断一下
            if (openList.IndexOf(end) > -1)
            {
                break;
            }
        }
    }

    private void PointsFilter(List<Point> src, List<Point> closeList)
    {
        foreach (Point p in closeList)
        {
            if (src.IndexOf(p) > -1)
            {
                src.Remove(p);
            }
        }
    }

    private List<Point> GetSurroundPoints(Point point)
    {
        Point up = null, down = null, left = null, right = null;
        Point lu = null, ru = null, ld = null, rd = null;
        if (point.Y < mapHeight - 1)
        {
            up = map[point.X, point.Y + 1];
        }
        if (point.Y > 0)
        {
            down = map[point.X, point.Y - 1];
        }
        if (point.X > 0)
        {
            left = map[point.X - 1, point.Y];
        }
        if (point.X < mapWidth - 1)
        {
            right = map[point.X + 1, point.Y];
        }
        if (up != null && left != null)
        {
            lu = map[point.X - 1, point.Y + 1];
        }
        if (up != null && right != null)
        {
            ru = map[point.X + 1, point.Y + 1];
        }
        if (down != null && left != null)
        {
            ld = map[point.X - 1, point.Y - 1];
        }
        if (down != null && right != null)
        {
            rd = map[point.X + 1, point.Y - 1];
        }
        List<Point> list = new List<Point>();
        if (down != null && down.IsWall == false)
        {
            list.Add(down);
        }
        if (up != null && up.IsWall == false)
        {
            list.Add(up);
        }
        if (left != null && left.IsWall == false)
        {
            list.Add(left);
        }
        if (right != null && right.IsWall == false)
        {
            list.Add(right);
        }
        if (lu != null && lu.IsWall == false && left.IsWall == false && up.IsWall == false)
        {
            list.Add(lu);
        }
        if (ld != null && ld.IsWall == false && left.IsWall == false && down.IsWall == false)
        {
            list.Add(ld);
        }
        if (ru != null && ru.IsWall == false && right.IsWall == false && up.IsWall == false)
        {
            list.Add(ru);
        }
        if (rd != null && rd.IsWall == false && right.IsWall == false && down.IsWall == false)
        {
            list.Add(rd);
        }
        return list;
    }

    private Point FindMinFOfPoint(List<Point> openList)
    {
        float f = float.MaxValue;
        Point temp = null;
        foreach (Point p in openList)
        {
            if (p.F < f)
            {
                temp = p;
                f = p.F;
            }
        }
        print("返回open列表中最小的f:" + temp.F);
        return temp;
    }

    private float CalcG(Point now, Point parent)
    {
        return Vector2.Distance(new Vector2(now.X, now.Y), new Vector2(parent.X, parent.Y)) + parent.G;
    }

    private void CalcF(Point now, Point end)
    {
        //F = G + H
        float h = Mathf.Abs(end.X - now.X) + Mathf.Abs(end.Y - now.Y);
        float g = 0;
        if (now.Parent == null)
        {
            g = 0;
        }
        else
        {
            g = Vector2.Distance(new Vector2(now.X, now.Y), new Vector2(now.Parent.X, now.Parent.Y)) + now.Parent.G;
        }
        float f = g + h;
        now.F = f;
        now.G = g;
        now.H = h;
    }
}

引用自 https://blog.csdn.net/truck_truck/article/details/77373514#

十二、游戏内的常用系统

1. 背包系统

1.1 UI搭建

1.2 拖拽物品

使用OnDrag()方法,将需要移动的物体的位置改变

1.3 物品交换

需要在一个能够拿到所有物品Image的父物体上挂control脚本,在脚本中处理交换逻辑。每个子物体中挂相同脚本,在OnPointerEnter方法中设置target值,在OnPointerExit方法中将target设置为null,在OnBeginDrag方法中设置Original,在OnDrag方法中更改original的位置,在OnEndDrag方法中决定要不要交换。关键细节:需要将在上的物体的Raycast Target取消勾选,否则会影响OnPointerEnter方法中的target设置

1.4 制作背包系统的常用思路

设置一个子物体序号最高的Image类型UI的“temp”用于拖拽,大小和想拖动的Image一样,开始的时候取消激活。序号最高决定了拖拽的时候始终在上,将temp的Raycast Target取消勾选,OnBeginDrag中将temp放到original的位置,temp的图片设置成original的图片,将original的图片隐藏,OnDrag中改变的是temp的位置,OnEndDrag中再将temp取消激活,将original还原即可

附代码实现:

  • Item.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class Item : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler , IPointerEnterHandler, IPointerExitHandler
{
    //获得脚本
    private ItemManager _manager;

    private void Awake()
    {
        _manager = transform.parent.parent.parent.GetComponent<ItemManager>();
    }

    // 开始拖拽
    public void OnBeginDrag(PointerEventData eventData)
    {
        // 设定交换源
        _manager.Original = transform;
        // 通知开始交换
        _manager.BeginDrag();
    }

    // 拖拽中
    public void OnDrag(PointerEventData eventData)
    {
        // 调用拖拽中的方法
        _manager.OnDrag(eventData.position);
    }

    // 结束拖拽
    public void OnEndDrag(PointerEventData eventData)
    {
        // 调用结束方法
        _manager.EndDrag();
    }

    // 鼠标划入
    public void OnPointerEnter(PointerEventData eventData)
    {
        // 设定target
        _manager.Target = transform;
    }

    // 鼠标划出
    public void OnPointerExit(PointerEventData eventData)
    {
        // 将target放空
        _manager.Target = null;
    }

}

  • ItemManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class ItemManager : MonoBehaviour {

    // 发起交换的源
    private Image _original;
    // 交换的目标
    private Image _target;
    // 用于临时拖拽的Image
    private Image _temp;

    public Transform Original
    {
        set
        {
            _original = value.GetComponent<Image>();
        }
    }

    public Transform Target
    {
        set
        {
            if (value != null)
            {
                _target = value.GetComponent<Image>();
            }
        }
    }

    // Use this for initialization
    void Start () {
        // 获取temp
        _temp = transform.Find("Temp").GetComponent<Image>();
    }

    // 开始拖拽
    public void BeginDrag ()
    {
        // 判断是否是空物体
        if (_original.sprite.name == "Character_Slots_Background")
        {
            // 是拖拽的空物体,则什么都不做直接跳出
            return;   
        }
        // 隐藏original的图片
        _original.enabled = false;
        // 把temp位置设置到original的位置
        _temp.transform.position = _original.transform.position;
        // 把temp图片换成original的图片
        _temp.sprite = _original.sprite;
        // 把temp激活
        _temp.gameObject.SetActive(true);
    }

    // 拖拽中
    public void OnDrag (Vector2 position)
    {
        // 移动temp的位置
        _temp.transform.position = position;
    }

    // 结束拖拽
    public void EndDrag ()
    {
        // 因为之前拖拽物体判断是空物体的时候直接跳出,所以没有将
        // temp激活,所以当temp为非激活的物体时,什么都不用做,直
        // 接跳出
        if (_temp.gameObject.activeSelf == false)
            return;
        // 判断是否要交换
        if (_target != null)
        {
            Debug.Log("Need Swap");
            // 需要交换
            _original.sprite = _target.sprite;
            _target.sprite = _temp.sprite;
        }
        // 还原original
        _original.enabled = true;
        // 隐藏temp
        _temp.gameObject.SetActive(false);
    }
}

2. 第一人称 FPS Demo

  • 第一人称其实是在控制摄像机的移动,在官方的标准资源包里有一个设置好的第一人称镜头(Standard Assets/Character/FirstPersonCharacter)
  • 枪作为摄像机的子物体,可以实现随摄像机旋转枪也跟着旋转
  • 换枪就是激活要换的枪的GameObject,取消激活上一次的枪的GameObject
  • 开枪的动画,音效随着点击鼠标而触发,需要添加一个计时器控制射速
  • 刚换枪和换子弹的时候不能开枪,需要在状态机中两个状态的Behaviour中控制
  • UI搭建子弹数,血量等内容

3. 塔防Demo

  • 地图搭建
    1. 烘焙地图
  • 摄像机控制
    1. CameraControl.cs
  • 生成怪物
    1. MonsterInfo.cs,不继承Monobehaviour,做每波的波次信息的类,整个类Serializable
    2. WaveInfo.cs,用集合存所有的波次信息,做单例
    3. MonsterInit.cs,生成怪物,在start点生成
    4. Monster.cs,控制每个怪物的行为,在生成的时候挂给每一个怪物
    5. EndCheck.cs,怪物走到终点,销毁
  • 生成防御塔(与生成怪物差不多)
    1. TowerInfo.cs,存储防御塔的信息类
    2. TowerCollection.cs,单例,用集合存所有的塔的信息
    3. TowerInit.cs,防御塔生成
    4. Tower.cs,控制每个塔
    5. TowerAttack.cs,防御塔攻击
    6. Bullet.cs,控制炮弹的脚本
  • 玩家血量
    1. PlayerInfo.cs,记录玩家的信息
  • UI
    1. 倍速按钮
    2. 暂停按钮
    3. 炮塔列表侧边栏

十三、插件

1. DOTween

DOTween is a fast, efficient, fully type-safe object-oriented animation engine for Unity, optimized for C# users, free and open-source, with tons of advanced features

附:有关脚本的一些琐碎知识点

1. this和gameObject区别

  • gameObject:当前脚本挂载给的游戏对象,类型是GameObject
  • this:当前脚本的对象,类型是脚本类型

2. gameObject可以省略

gameObject.GetComponent<BoxCollider>();gameObject.可以省略

3. .gameObject.transform

  • 任意的组件对象,都可以通过.gameObject获取到游戏对象,
  • 任意的游戏对象,都可以通过.transform获取到transform组件

4. 在Inspector面板中设置脚本内字段

  • 只有用public修饰的字段会在面板中出现,并且List<T>会自动实例化
  • 只要面板中编辑过该字段的值,脚本代码无论改什么值,面板中都不会改变
public string name = "hello world";
public bool c;
public GameObject d;
public BoxCollider e;
public List<int> f;

public List<Person> p;
[System.Flags]
public enum Gender{
    Male, Female
}
[System.Serializable]
public class Person{
    public string name;
    public int age;
    public Gender gender;
}

5. 特性的使用

  • 给类添加
// 不允许重复添加多个组件
[DisallowMultipleComponent]
// 组件依赖,添加的时候自动添加刚体组件
[RequireComponent(typeof(Rigidbody))]
  • 给字段添加
// 在Inspector面板给字段添加说明头
[Header(string description)]
// 限制字段范围(浮点型和整型可以用)
[Range(float min, float max)]
// Inspector面板两行之间的间距
[space(100)]
// 可序列化,使得自定义类可以显示在Inspector面板上
[System.Serializable]
// 在Inspector面板上隐藏
[HideInInspector]

6. 单例脚本

如果一个脚本想要在其它脚本中频繁使用,可以做一个单例脚本

public static Joystick Instance;

private void Awake()
{
    Instance = this;    // this 指脚本对象
}

7. 设计模式

高内聚,低耦合

7.1 MVC

  • Model:数据层,用来管理数据
  • View:视图层,用来显示数据
  • Controller:控制层,用来控制将数据给视图展示,协调数据层和视图层

7.2 MVVM

  • Model:数据层
  • View:视图层
  • View-Model:绑定在视图层

8. 游戏物体标签

游戏物体在Inspector面板上第一个图标即为设置图标标签的地方,选择后会在Scene面板中显示一个图标,可以快捷的选中所设置的游戏物体,避免了多部分组成的模型点选时选不到总体的游戏物体,在搭建场景的时候可以提高效率

推荐阅读更多精彩内容