unity-基础知识

ooowl
  • 游戏开发
  • unity
About 56 min

Unity-基础知识

常识

很多都是参数介绍,看的时候打开Unity对着面板看一眼
工程文件夹都是干什么的

  • Assets:美术资源脚本等等,主要的工程文件夹,只要它不丢剩下的都删除了再打开也会自动生成
  • Library:库文件夹
    • Library/PackageCache下面的包是unity自己下载的,每次重新进入都会重新加载检查,所以这里面缺斤少两删除重进就行
  • Logs:日志文件夹,记录特殊信息
  • obj:编译产生中间文件
  • Packages:包配置信息
  • ProjectSettings:工程设置信息,删了自动创建会丢失自定义的设置

重要的几个面板

  • Hierachy所有游戏中对象的层级面板
  • Sence当前的游戏场景
  • Game当前游戏画面会如何
  • Project项目中的所有资源
  • Inspector某一个对象/资源的属性
  • Window->Package Manager 管理安装的第三方包
  • 给unity添加一些组件现在是在File->BuildSettings里面找到

Sence中常用快捷键

  • alt+滚轮缩放
  • alt长按可以围着视觉中心 转
  • 鼠标右键+wasd移动(shift可以加速)
  • 鼠标中键 原地不动360度拖动场景
  • ctrl+D快速复制
  • F2快速改名
  • ctrl+shift+F快速对焦摄像机到编辑模式视窗
  • 按住V可以选择顶点吸附,双击鼠标中键或F键可以定位场景内的物体

面板

Sence的工具栏中比较重要的几个
shading mode,默认shader渲染,可以切换网格/混合渲染,后面的是2D视图 声 光 特效 辅助线开关,摄像机调整。
Gizmos是用来设置Sence中不同类型的GameObject图标的, 一般这个工具栏的东西都不用特殊调整。
轴向以屏幕为参照物:

  • 垂直屏幕向内为Z正方向
  • 平行屏幕向右为X正方向
  • 平行屏幕向上为Y正方向 操作qqq物体工具栏的快捷键竟然是QWERTY(????
    可以切换操作物体的轴是以自己为准还是以世界坐标为准,按单位移动需要使用世界坐标,按住ctrl按单位移动的精细一些。
    使用pivot或者center,可以切换transform是以对象组的根对象为中心还是对象组的中心
    眼睛和手是是否可以看见和是否可选中

Game界面⏯是逐帧播放,status是渲染详细信息
project的更多选项(三个点)可以切换界面显示方式,可以选择两列显示,Asset alt缩放到最小按照行显示

Inspector面板,可以设置在场景设置中的图标,Layer系统和Tag系统可以给对象分类,可以在更多中设置Debug显示更多信息方便调试

顶栏菜单额外嘱咐的

  • Edit->Selection: alt+ctrl+1 编队(save selection 1) shift+ctrl+1 选中编队(load selection 1) 在mac上是command
  • Edit->ProjectSetting设置保存到Asset同级的文件夹ProjectSetting
  • Edit->Preferences
  • Assets->import/export package
  • GameObject->ctrl+shift+F把对象(大部分时候是摄像机)视角和当前的Sence视角重合
  • Window->General上述几个窗口都在里面显示
  • Window->Analyze->Profiler 这是性能监控器

对象关系
transform是最基本的位置信息。物体拿到手transform先归零好判断位置,子对象的transform是针对父对象来说的,会根据父对象变化而变化。
unity的基本单位是米,体积是基本单位就是立方米 在 Unity 外部创建模型 - Unity 手册open in new window

Warning

无论是美术还是自己拼模型,模型的正方向一定是Z轴的正方向,也就是垂直纸面向内。和开发者的目光齐平,缩放单位要注意!
骨骼-非必须,有动作的模型才需要
网格面片-必须,决定了模型的轮廓
贴图-必须,决定了模型的颜色效果
官方推荐使用FBX格式的模型文件其它格式虽然支持,但是不推荐。[.fbx .dae .3ds .dxf .obj]
导入完之后看看他有没有给我做好了预设体,别傻乎乎自己再去重复劳动
最好使用空物体作为总的父对象,容易控制缩放

基础操作

Unity利用反射获取 场景中(Sence.unity) 的所有自定义的 GameObject和挂载其上的脚本,所有的特征都是GameObject +Component呈现出来的
场景的本质是记录了 GameObject和关联的信息存起来的文件,后缀是Sence.unity 把两个场景都拖到Hierachy中可以让场景叠加显示,一般只有互相copy的时候才这么干
public的属性能在inspector上编辑,加上[HideInspector]就隐藏了;private的和protected不能被显示,加上[SerializeField]也可以被显示
自定义的结构体和类对象使用[System.Serializable]就可以被显示了。
已经挂到 GameObject修改了脚本的值对象上挂载的那个不会被更改,手动或者重挂,也可以运行过程中可以用Inspector->copy和paste脚本,但是一般不会这么做
departure的方法如果硬要用会报错,但是传第二个参数为false就可以继续用虽然不推荐

📌Tip

其实直接拖对象有点破坏面向对象思想,尤其是多人合作依赖关系容易变成一坨,而且从外边拖进来的你还没法从代码直接点过去
代码Find之后再改效率不如拖高,因为不用查找(其实查找也查找不了几个最多在自己的子去查找)
在Editor绑定的好处是程序做完了,给个毛坯给策划,让他们自己去精装,把体力活交给策划,程序只负责实现功能就行了。

Click to see more

还有一些attributes不太重要的有个印象就行

  1. 为变量分组[Header("分组说明")]
  2. 为变量添加鼠标悬停说明[Tooltip("说明内容")]
  3. 让两个字段间出现一点间隔[Space()]
  4. 数值变为使用滑动条范围设置[Range(最小值, 最大值)]
  5. 显示框大一点,默认不写参数时显示3行,写参数则对应显示相应行数[Multiline(4)]
  6. 也是显示框默认不写参数时超过3行显示滚动条;可以指定最少和最多显示行数,超过最大行数则显示滚动条[TextArea(3, 4)]
  7. 右键变量的时候执行方法function(无返回值无参)[ContextMenuItem("右键显示啥名", "function")]
  8. 为方法添加特性能够在Inspector中直接点更多去执行[ContextMenu("测试函数")]

Perfab

Perfab的存储规则和Sence是一样的,C#脚本组件任何东西都一块保存。
在Inspector面板中可以把当前选中的预设体的更改保存到预制体或者放弃更改重新拖进Assets中是覆盖。如果删除或者修改已有预设体,那所有的预设体都会被修改
在场景中直接unpack perfab然后编辑,再拖进Assets搞个新的。把未修改预设体拖回去是复制一个一模一样的预制体。如果预设体被删除但是场景中已经使用了,就会变红提示,不想要就直接unpack。

组件

Warning

在Unity中类名和文件名必须一致,因为是通过反射字符串去获取类和自定义组件的

自己写的脚本实际上也是组件(Component)继承了MonoBehaviuor的都是组件。 一个脚本可以挂在同一个对象多次,也就会创建多个脚本对象,可以脚本中开[DisallowMultipleComponent]特性就不能一挂多了。其实本质是拖过去的时候通过反射获取文件的名字,然后根据名字字符串拿到对应类
MonoBehaviuor的继承链是MonoBehaviuor <- Behaviour <- Component ,继承Component的就是组件。当然可以继续继承
集成了MonoBehaviour的类只能挂载不能new,不能自定构造函数。 在Inspector->ExecutionOrder中其实可以规定执行顺序的,默认顺序是不确定的,Editor/Data/Resources/ScriptTemplates可以修改默认脚本模板,这俩一般不用特意改。

生命周期

人眼舒适放松时候可视帧数是24fps,游戏卡顿的原因就是一帧内的计算量过大没法处理完所有的游戏逻辑。
生命周期函数(是按下面顺序执行的!!!!):

  1. Awake:首次被产生执行一次,再次启用就不会执行(如果生产对象是一开始inactivate的,那也不会执行)
  2. OnEnable: 依附的GameObject每次activate时调用,可以用来Fetch资源
  3. Start:从自己被创建出来后,第一次帧更新之前调用只会调用一次
  4. FixedUpdate: 固定时间(0.2)刷新一次,可在edit->project setting->time更改间隔
  5. Update: 每帧执行一次(注意性能
    • Unity在这两个生命周期之间进行动画系统的更新
  6. LateUptedate: 每帧后执行一次
    • 所以为了渲染效果一般在这里更新摄像机的位移
  7. OnDisalbe: 依附的GameObject每次禁用activate事件执行,在OnDestory时也会执行一次
  8. OnDestory:销毁的时候执行

生命周期函数是支持继承和多态的, 子类继承类之后进行override就可以。
但是生命周期函数却不是从基类中override过来的
如果生命周期无法满足需求那就需要使用任务调度,时间任务调度函数Invoke InvokeRepeating CancelInvoke (参数都简单易懂

Click to see more

生命周期

using System;  
using System.Collections;  
using System.Collections.Generic;  
using UnityEngine;  
  
public class TestLifeCycle : MonoBehaviour  
{  
    void Awake() // 首次被产生或者从不可见变为可见的前(核心就是首次启用)执行一次,再次启用就不会执行,例如查找资源用  
    {  
        Debug.Log("I'm awake");  
    }  
    private void OnEnable() //  每次启用前的函数,可以用来Fetch资源  
    {  
        Debug.Log("I'm Ready!");  
    }    
    void Start() // GameObject加载完毕后所有的Awake执行完的第一帧执行一次  
    {  
        Debug.Log("I'm Start First Time!");  
    }  
  
    private float timedelta = 0.2f;  
    void FixedUpdate()  
    {        
        Debug.Log($"every {timedelta} second Update once");  
    }    // Update is called once per frame  
    void Update()  
    {        
        Debug.Log("frame start");  
    }  
    void LateUpdate()  //  每帧之后  
    {  
        Debug.Log("frame end");  
    }  
    void OnDisalbe()  
    {        
        Debug.Log("I'm Disabled");  
    }  
    void OnDestory()  
    {        
        Debug.Log("I'm Destory");  
    }
}

运行结果

[21:35:31] I'm awake
[21:35:31] I'm Ready!
[21:35:31] Object start successfully Complete
[21:35:44] every 0.2 second Update once
[21:35:44] frame start
[21:35:44] frame end
[21:35:40] be Invoked
[21:35:35] be Invoked Hello, World!
[21:35:42] Invoke is Canceled

获取组件

MonoBehaviuor内置的重要变量,它能获得什么属性去IDE里看,稍微列一下最常用的,不赘述。this不是依附的GameObject
Component中声明的(this.)gameObject可以直接使用,这个对象就是脚本挂载的对象。transform是获取位置信息
如果要获得其他的GameObject挂载脚本的对象,声明一个对应类然后把对象拖过去就可以。获得GameObject上的其他的 Component有一系列方法,参考一下,最常用的是根据泛型获得

Click to see more

如何获取GameObject身上的组件

void Start()  
{  
    print(gameObject.name);//  挂载GameObject的名字  
    print(this.transform.position);//  对象的位置  
    print(this.transform.eulerAngles);//  旋转欧拉角  
    print(this.transform.lossyScale);//  缩放  
    print(this.enabled);// 脚本是否被启用  

    // 获得其他组件
    GTest g1 = this.GetComponent("GTest") as GTest; //  通过名字获得  
    GTest g2 = this.GetComponent(typeof(GTest)) as GTest;//  通过类型获得  
    GTest g3 = this.GetComponent<GTest>(); //  ⭐️通过泛型获得  
    GTest[] g4 = this.GetComponents<GTest>(); // 获得多个,但是无法确定是哪一个  
    GTest[] g5 = GetComponentsInChildren<GTest>(true); // 递归查找所有子对象包括自己的组件,参数代表要不要查找未激活的对象的  
    GTest[] g6 = GetComponentsInParent<GTest>(true); // 递归查找所有父对象包括自己的组件,参数代表要不要查找未激活的对象的  
    GTest gt;
    bool exists = TryGetComponent<GTest>(out gt); // 尝试获得,返回值是bool
}

MonoBehaviour内置属性

GameObject

GameObject的一些静态方法。
可以直接申请一个对象然后拖进去(最常用的),或者使用下面的方法查找
常用的属性和查找对象是找不到incativate的对象的,如果场景中多个满足条件的对象,则无法确定是哪个
动态创建对象,先拖过去获取 GameObject然后使用GameObject.Instantiate(obj)初始化新的,名字会带(Clone)
GameObject.Destory(obj)可以动态删除对象甚至可以删除附加的脚本,实际上是下一帧移除的,立即从内存中移除需要使用GameObject.DestroyImmediate(mycube);
这俩函数实际上有很多重载方法。
默认切换场景的时候之前的都被移除了,如果不想某对象换场景被移除,使用GameObject.DontDestroyOnLoad(this.gameObject);最常用的就是传自己

Click to see more

GameObject的操作

public class L4Script : MonoBehaviour  
{  
    private GameObject mycube;  
    public GameObject find;  
    public GameObject[] finds;  
  
    public GameObject dynamicObj;  
  
    public GameObject dynamicObjClone;  
  
    void Start()  
    {        
        Debug.Log(gameObject.name);  
        Debug.Log(gameObject.isStatic);  
        Debug.Log(gameObject.activeSelf);  
        Debug.Log(gameObject.layer); // 这返回的其实是个整数  
        Debug.Log(gameObject.tag);  
        Debug.Log(gameObject.transform.position);  
  
        mycube=GameObject.CreatePrimitive(PrimitiveType.Cube); // 和右键点击创建一样,也能创建其他类型  
        mycube.name = "xx";  
        mycube.tag = "Monster";  
  
        // 查找 是找不到失活对象,如果场景中多个满足条件的对象,则无法确定是哪个  
        find = GameObject.Find("xx");//  需要遍历所有对象,运行效率比较低 一般不这么找  
        find = transform.Find("xy"); // 只寻找自己的子对象,在子对象和坐标那有详细的
        find = GameObject.FindWithTag("Monster"); //  一样FindGameObjectWithTag  
        finds = GameObject.FindGameObjectsWithTag("Monsters"); // 只有通过tag查找能找到多个对象  
  
        L4Script l4 = FindObjectOfType<L4Script>(); // UnityEngine.Object的查找更费劲,不仅遍历对象还遍历脚本  
  
        dynamicObjClone=GameObject.Instantiate(dynamicObj); // 获取到对象然后初始化的方法  
        GameObject.Destroy(dynamicObjClone,5); // 五秒后删除  
        GameObject.Destroy(this);//  甚至可以删除附加的脚本  
        GameObject.DestroyImmediate(mycube);// 立即从内存中移除,更可能会卡顿  
        GameObject.DontDestroyOnLoad(this.gameObject);// 最常用的就是传自己  
    }
}

当你new GameObject的时候实际上就创建了一个 空物体 甚至创建之后可以挂脚本。甚至脚本中套娃创建都能执行
动态的为对象添加组件(脚本也是组件),拿到组件就可以更改组件的属性,从而控制行为。

Click to see more

动态创建GameObject

void Start() 
{  
    GameObject ng1 = new GameObject("我创建的新的空对象"); // 创建了一个新的 空对象  
    ng1.name = "我创建的新的空对象1";  
    GameObject ng2 = new GameObject("我创建的新的空对象",typeof(GTest));  
    GTest gt1=ng1.AddComponent(typeof(GTest)) as GTest; // 动态添加组件  
    GTest gt2=ng1.AddComponent<GTest>(); // 使用泛型动态添加组件更常用  
    Debug.Log("success init");  
  
    if (find.tag == "Monster")//  标签比较  
    {  
        bool TagEqual = find.CompareTag("Monster"); // 和上面的条件一样  
        print($"Tag is Monster{TagEqual}");  
    }  
    find.SetActive(false); // 激活函数不多说  
    find.SetActive(true); 
}

📄Info

不常用的报个菜名

this.gameObject.SendMessage("TestFun");通过反射去动态调用这个 GameObject脚本上的函数
this.gameobject.BroadcastMessage("func");广播行为 让自己和自己的子对象执行
this.gameobject.SendMessageupwards("func");向父对象和自己发送消息并执行

tag和layer

每个GameObject都有这两个属性,可以在Inspector面板进行编辑,可以给Layer起名字。

obj.layer = LayerMask.NameToLayer("LayerA"); //给物体设置层

int l=LayerMask.NameToLayer("UI"); // LayerToName
Physics.OverlapBox(Vector3.zero,Vector3.one, Quaternion.AngleAxis(45,Vector3.up),   
1<<LayerMask.NameToLayer("UI") | 1<<LayerMask.NameToLayer("Default") // 检测UI和Default层0000 0000 0000 0010 0001
);

注意此处数字层的含义,这个int代表的是此层的二进制哪一位是1,unity一共32层,如果你要检测第五层和第1层,则需要如上述代码一样,将层数二进制左移对应数,然后两层进行或操作,这样用一个二进制就可以表示你想检测的所有层,unity内部会进行与操作再去检测

SortingLayers相当于PS中层的概念,每个Object属于哪个层,最下面的从层最靠前

Time和Mathf

在编辑模式下,是不限帧率的。Time用于游戏中参与位移、记时、时间暂停等,Mathf和Random都是Unity命名空间下的不是C#自带的
Random.Range(start,end) 传浮点返浮点,传整数返整数;整数有头没尾,浮点有头有尾。

Click to see more

Time的基本API用法

void Update()  
{  
    Time.timeScale = 1.0f; // 缩放倍数0的时候停止  
    print(Time.deltaTime); // 最近两帧间隔多长时间,稳定应该是 (1/帧率)S,主要是用来计算位移的 路程=时间*速度  
    print(Time.unscaledDeltaTime);// 不受timeScale影响的帧间隔时间,如果希望游戏暂停了还跑的东西  
    print(Time.fixedTime);//  两个FixedUpdate的间隔时间  
    print(Time.fixedDeltaTime); // 物理帧间隔,受scale影响  
    print(Time.fixedUnscaledDeltaTime); // 物理帧间隔,不受scale影响  
    print(Time.frameCount);// 游戏到现在跑了多少帧了  
    print(Time.time);// 游戏开始到现在过了多少时间  


    // Random
    Random.Range(start,end)

}

Mathf

Math是System中提供的数学,而Mathf是unity提供的结构体,二次封装且多了一些游戏常用的,用他就行。
取整数 直接类型转换 和 Mathf.FloorToInt(9.6f)是向下取整,而CellToInt是向上取整。

Click to see more

数学库Mathf

Mathf.PI  
Mathf.Abs(-10) // 取绝对值  
Mathf.CeilToInt(1.00001f)//向上取整 Mathf.FloorToInt(9.6f)//向下取整  
// 钳制函数如果不超过区间就返回传入的数字,否则返回靠近的区间端点  
Mathf.Clamp(10, 11, 20) Mathf.Max(1, 2)  
Mathf.Min(1.1f, 0.4f)  
Mathf.Pow(4, 2)// 一个数的n次幂  
Mathf.RoundToInt(1.3f) // 四舍五入  
Mathf.Sqrt(4) // 返回一个数的平方根  
Mathf.IsPowerOfTwo(4)//  判断一个数是否是2的n次方  
Mathf.Sign(0)//  判断正负数

// 每帧改变start的值按照极限逼近lim 
start = Mathf.Lerp(start, 10, Time.deltaTime);
//  结果会线性跟随
time += Time.deltaTime; 
result = Mathf.Lerp(start, 10, time);

Lerp插值公式为result = start + (end - start) * t;其中t为速度系数,常用的两个调用方式取值如下两曲线,lim(每帧改变start)和线性(每帧改变t)
1100

弧度rad即是PI/180,使用静态变量Rad2Deg = 57.29578f;Deg2Rad = 0.017453292f;一乘即可和角度互相转换;Mathf.Sin(r)参数和返回值都是rad类型,可以传30*Mathf.Deg2Rad

Transform

Vector3是Unity内置的结构体,他不是类!默认值(0f,0f,0f),初始化的时候传xy则z自动为0,默认的加减乘除就是对应坐标加减乘除,重载了哪些运算符点进去看。

Vector

Vector是Unity中的结构体,可用的有Vector2 Vector3 Vector4。
向量已经进行运算符的重载,使用B-A可以获得A指向B的向量,取反会三个方向都加负号,也就是指向反方向。
使用V.magnitude直接获得向量的模长和Vector3.Distance(Vector3.zero,V)一样,也就是三维空间中的距离Mathf.Sqrt(x^2+y^2+z^3)
向量除自己的模长可以归一化为单位向量,使用V.normalized获得,和V/V.magnitude一样

Click to see more

向量演示

Vector3.zero // (0, 0, 0) 零向量
Vector3.right // (1, 0, 0)
Vector3.left // (-1, 0, 0)
Vector3.forward // (0, 0, 1)这是Unity的正方向
Vector3.back // (0, 0, -1)
Vector3.up // (0, 1, 0)
Vector3.down // (0, -1, 0)
Vector3.Distance(v1,v2)// 计算两点之间的空间距离,和V.magnitude一样
V.normalized==V/V.magnitude //  归一化 true

float result=Vector3.Dot(A,B); //  A•B点乘的用法
Vector3 V=Vector3.Cross(A,B) // A*B叉乘的用法



点乘 获得的是对应坐标相乘再相加的一个标量,A•B的几何意义是B在A上的投影长度,因为AB是向量,所以可以用来判断AB的夹角>0锐角=0直角<0钝角。
可以大致判断物体的方位,物体面朝向是A然后另一个物体的坐标是B,此时使用A•B就可以大致判断方位
如果要做侦测是否在角度范围内,注意到Cosβ=A•B使用反三角函数直接算出来,但是其实可以调用Vector3.Angle(A,B)其实也使用点乘获得的
叉乘 每个纬度原来的数值不会参与运算即X=Ya​Zb​−Za​Yb​; Y=Za​Xb​−Xa​Zb​; Z=Xa​Yb​−Ya​Xb;使用Vector3 V=Vector3.Cross(A,B),新的向量V是AB的法向量,也是AB平面的法向量,若A在B的右侧 则法向量是垂直向上的反之则向下
假设向量 A 和 B 在 XZ 平面内,如果算出来Y>0则说明向量 B 在向量 A 的右侧反之在左侧,那就可以结合点乘判断物体距离角度和左右侧。
Lerp 插值运算,对两个坐标点进行插值运算,和Mathf的Lerp一样,只不过是三维的。

Warning

注意线性插值瞬移问题
线性插值可以用来做一些摄像机的跟随移动这样的东西。
当线性插值t累加>=1的时候,公式可以直接抵消,result=start+(end-start)*1 = end+O(t),如果是向量Lerp则两个物体会直接重合起来没法分开了。
可以设置一些条件,让t和开始位置重置一下避免这个问题。

球形插值 Vector3.Slerp(A.position,B.position,Time.deltaTime);不常用,可以让物体A呈弧线移动到位置B,比如模拟东升西落。

transform

transform.position在是世界坐标的位置,而transform.localPosition是相对于父对象的坐标,也就是面板上的。虽然可以单独调用,修改的时候只能通过position属性不能单独改某个维度的坐标
注意transform.forward/up/right这三个是 GameObject自身的正方向,使用Vector3.forward/up/right是世界坐标正方向。使用transform.Translate()封装好的API移动
相对世界坐标角度transform.eulerAngles和相对父对象的角度(面板上是这个)transform.localEulerAngles角度的修改和坐标的修改差不多
transform.lossyScale相对于世界坐标的缩放,不能修改(那为什么要提供?transform.localScale自己的缩放 没有提供API,只能自己慢慢每帧写
transform.LookAt(Vector3.zero) 一直看向某个点,传进某个 GameObject就会一直盯着看,重载了的也可以传position。

📝Note

使用eulerAngles属性获得的始终是0-360度的角度,但是面板上是可以出现负数的,需要自己写一些逻辑转换角度

自身方向的偏转角是怎么算出来的? #todo

Click to see more

Transform的基本API用法

void Update()
{
    // 每帧 移动 1/帧率*(0,0,1) 也就是每秒1单位(米)
    transform.position += transform.forward * (Time.deltaTime * 1f); 
    // transform封装好的和上面一样,一般用它位移,参数控制相对于世界坐标还是自身坐标
    // 注意这里就有排列组合了,以[自己or世界]坐标中心,偏移[自己or世界]朝向运动
    transform.Translate(transform.up * (Time.deltaTime * 1f),Space.Self); 
    
    // 1. 相对于世界坐标系的z轴移动,始终是朝世界坐标系的z轴正方向移动
    transform.Translate(Vector3.forward * (1 * Time.deltaTime), Space.World);
    // 2. 相对于世界坐标的自己面朝向动,transform.forward会先转换为等效世界坐标系下的方向向量,然后始终朝自己的面朝向移动
    transform.Translate(transform.forward * (1 * Time.deltaTime), Space.World);
    // 3. 相对于自己的坐标系下的自己的面朝向量移动(一定不会这样让物体移动,因为Z轴偏转被算了两次
    transform.Translate(transform.forward * (1 * Time.deltaTime), Space.Self);
    // 4. 相对于自己的坐标系下的z轴正方向移动,始终朝自己的面朝向移动
    transform.Translate(Vector3.forward * (1 * Time.deltaTime), Space.Self);


    print(transform.eulerAngles);// 相对世界坐标角度  
    print(transform.localEulerAngles);//  相对父对象的角度,面板上是这个
    transform.Rotate(new Vector3(0,10,0),Space.Self); //  绕自己坐标的Y轴转  
    transform.Rotate(new Vector3(0,10,0),Space.World); // 绕世界坐标的Y轴转  
    transform.Rotate(new Vector3(0,10,0),10*Time.deltaTime,Space.Self); // 重载 绕着哪个轴,转多少度,相对哪个坐标  
    transform.RotateAround(new Vector3(0,0,0),new Vector3(1,1,1),100*Time.deltaTime);// 相对于哪个点,哪个轴,旋转的角度

}

Quration

相比于欧拉角可以提供Lerp。
空间中的角旋转都可以分解为绕XYZ三个互相垂直的三个旋转角组成的序列。最常用的旋转序列约定就是heading-pitch-bank(Y-X-Z)约定,transform.eulerAngles欧拉角的缺点是同一旋转的方式不唯一 万向节死锁(万向锁)。
在unity中将物体的X轴调到90度会出现此现象,此时转动YZ都是绕Z转。

Warning

Gimbal lock问题
理解此问题的关键在于了解欧拉角旋转时的层级关系,当规定欧拉(2,1,3)时,其旋转层级关系可简单表述为y->x->z,即前者为后者的父层级。父层级的旋转会带动所有子层级旋转,而子层级的旋转不影响父层级的状态。
当x轴旋转时, 有概率使得z轴所在环与y轴所在环共面 ,此时即发生万向节死锁问题,导致z轴功能与y轴功能基本等效,即z轴失效。
此时若要旋转到需要的位置必须同时旋转3个轴,但这种旋转过程在实际空间中轨迹为弧线,不满足实际工程应用(比如摄像机转动会在球面上画个S弧而不是直线,运镜就很怪)

此时我们要用四元数解决这个问题,四元数包含一个标量和一个3D向量[w,(x,y,z)]代表了3D空间中的一个旋转量,在3D空间中,任意旋转都可以表示绕着某个轴v旋转一个角度w(轴-角 对)得到,因为只有一个轴所以避免了轴面重合
对于给定旋转,假设为绕着 N轴旋转 β 度,N轴为(x,y,z),那可以构成四元数 Q=[cos⁡(β/2),sin⁡(β/2)N]具体展开 Q=[cos⁡(β/2),sin⁡(β/2)x,sin⁡(β/2)y,sin⁡(β/2)z],第一个元素为四元数的实部,后面三个为虚部。
两个四元数相乘代表旋转四元数,四元数把角空间转换到180 ~ -180 此时位置的表示是唯一的,解决了 同一旋转表示不唯一 的问题。

单位四元数即[1,(0,0,0)] [-1,(0,0,0)]角度为0或者360度,没有旋转量的,使用Quaternion.identity获得,初始化对象的时候可以传入赋值
四元数同样提供了插值运算,即角度转向过程的插值,Quaternion.LerpQuaternion.Slerp。Lerp更快 但是如果旋转范围较大效果较差,建议首选Slerp进行插值运算。
注意LookRotation的运算,当A朝向B的时候,AB向量为轴,A可以绕此轴旋转自身,此时使用upwards规定Y轴也就是上的方向,三个角度就都就确定了,默认upwards是世界坐标的上方向

Click to see more

四元数相关操作

void Start()  
{  
    Vector3 fake_part=Mathf.Sin((60 / 2)*Mathf.Deg2Rad) * Vector3.right;  
    Quaternion q1 = new Quaternion(fake_part.x,fake_part.y,fake_part.z, (60 / 2)*Mathf.Deg2Rad); 
    Quaternion q2 = Quaternion.AngleAxis(60, Vector3.right); // 这种初始化常用,上面的不常用
    Quaternion q3 = Quaternion.Euler(new Vector3(30,30,90)); // 欧拉角 转 四元数
    q3.eulerAngles // 获得 四元数 的 欧拉角 
    Quaternion.identity; //  单位四元数 
}  
void Update()  
{  
    transform.rotation*=Quaternion.AngleAxis(1, Vector3.up); // 四元数相乘,绕世界的Y轴转1度,每帧转  
    //  Lerp和前面几个Lerp参数一样。
    A.transform.rotation = Quaternion.Lerp(A.transform.rotation,target.transform.rotation,Time.deltaTime*0.3f); 
    B.transform.rotation = Quaternion.Slerp(B.transform.rotation,target.transform.rotation,Time.deltaTime*0.3f);
    // 传入自己 A指向B的向量 可以计算出 A面朝B 需要的四元数,可以做到lookAt的效果,其实LookAt就是通过四元数做的。
    A.transform.rotation=Quaternion.LookRotation(B.transform.position - A.transform.position, upwards:Vector3.up);
}

两个四元数相乘的结果,相当于绕物体自己的坐标系旋转角叠加后的四元数, 注意四元数运算永远是使用物体本地坐标系进行旋转的
使用向量乘四元数的结果,相当于把此向量旋转对应四元数的旋转量后的向量, 注意此运算不满足交换律四元数必须在前

子对象和坐标

当把子对象拖到世界空间的时候,位置信息是加法,而缩放信息是乘法的;如果你再拖回去,那么就倒过来算一次(拖到GameObject下同理)。
坐标转换的时时候,会受到缩放影响,应该是除了scale

Click to see more

也可以用代码完成拖动

void Start()  
{  
    print(transform.parent.name);// 获取父对象  
    transform.parent = null; // 脱离父对象  
    GameObject F2 = GameObject.Find("F2");  
    transform.parent = F2.transform; // 绑定父对象  
    GameObject F3 = GameObject.Find("F3");  
    // 如果是False,那么会不经过计算而是直接绑定对象关系然后把原来的transform赋值过去  
    transform.SetParent(F3.transform,false);  
    transform.DetachChildren(); // 把自己下一层的物体都脱离,非递归  
    transform.Find("S2"); // transform下的Find是查找子对象,且失活的也能找到,非递归  
    print(transform.childCount);// 获得自己的子对象数量  
    transform.GetChild(0); // 通过下标索引获得子对象,这样就可以便利了  
  
    transform.IsChildOf(F2.transform); // 判断是不是传入对象子对象  
    transform.GetSiblingIndex(); // 获得自己的子对象编号,当然也有Set,Set传入溢出的时候就成为最后一个  
    transform.SetAsFirstSibling();// 设置为第一个,设置为最后一个  


    //世界转本地 #转换坐标系,可以大概判断相对位置
    print(transform.InverseTransformPoint(Vector3.forward)); //  点转换
    print(transform.InverseTransformDirection(Vector3.forward));//  单位向量转换 不受缩放影响  
    print(transform.InverseTransformVector(Vector3.forward));//  单位向量转换 这个会受缩放影响
    // 本地转世界  
    print(transform.TransformPoint(Vector3.forward)); //  ⭐️点转换(在单位的某方向放效果,可以直接转为世界然后创建)
    print(transform.TransformDirection(Vector3.forward));//  单位向量转换 不受缩放影响  
    print(transform.TransformVector(Vector3.forward));//  单位向量转换 这个会受缩放影响
}

核心组件

Inupt和Screen

键盘鼠标:屏幕的原点是左下角,所有的输入都是在Input类中管理的,一般用键盘的枚举在KeyCode,用Input.anyKeyDownInput.inputString捕获输入设置快捷键可以这么干 在 ProjectSettings-> InputManager设置默认轴输入Input.GetAxis,Unity中使用轴向来代表输入的方向,键鼠有四个常用的,不用我们手动处理按键和移动距离直接返回Float,移动越快值越大
这个方法返回的值是会缓冲逐渐加速 在 -101 之间,如果使用Input.GetAxisRaw可以获得不带加速度的
注意怎么处理组合键,如果组合键非常多,可以先用变量都捕获,然后下面自己加一堆逻辑判断。什么多点触控屏幕陀螺仪手柄,用到了再说

Click to see more

Input的基本API用法

Input.mousePosition; // 获得鼠标在屏幕上的位置
Input.GetMouseButtonDown(0) // 0左键1右键
Input.GetMouseButtonUp(0) 
Input.GetMouseButton(0)  // 持续按下
if (Input.mouseScrollDelta!=Vector2.zero){  //  鼠标滚动返回的是Vector2,正数向上滚反之可推
    print(Input.mouseScrollDelta);// 滚动了才打印, 而且只变动Y值
}

Input.GetKeyDown(KeyCode.X) // 获得某个键的按下事件,只会触发一次
Input.GetKeyDown("b") // 也可以使用函数调用而不是枚举,大小写敏感,只接受小写字符串
Input.GetKeyUp(KeyCode.X) // 获得某个键的弹起事件
Input.GetKey(KeyCode.D)  // 长按理论上是一帧一次,但是会帧率太高的话Unity中会优化,让触发频率降低

/* 处理组合键 */
bool isCtrlPressed = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
bool isCPressed = Input.GetKey(KeyCode.C);
if (isCtrlPressed && isCPressed)Debug.Log("Ctrl + C is pressed.");

/* 下面这俩搭配可以记录快捷键 */
Input.anyKeyDown // bool 是否有任意键按下
Input.anyKey // bool 是否有任意键长按
if (Input.anyKeyDown) print($"Pressed {Input.inputString}"); // 记录下捕获的,他只会捕获上一个

string[] strs = Input.GetJoystickNames(); //  获得所有手柄的名字

/* 使用默认轴输入 */
print(Input.GetAxis("Horizontal")); // 默认AD  
print(Input.GetAxis("Vertical")); // 默认WS  
print(Input.GetAxis("Mouse X")); // 鼠标水平  
print(Input.GetAxis("Mouse Y")); // 鼠标竖直

Screen属性很简单就是宽高刷新率,注意设备分辨率和窗口的区别,自动旋转屏幕和运行时全屏也可以设置但是不太重要。

Click to see more

Screen的基本API用法

void Start()  
{  
    int h=Screen.width; // 当前窗口宽高  
    int w=Screen.height; // 当前窗口宽高  
    print($"运行窗口 高{h}{w}");  
    Resolution r = Screen.currentResolution; // 运行设备的宽高,如果是在编辑器里面是开发机器的屏幕  
    print($"执行程序的机器 高{r.height}{r.width} 分辨率{r}");  
    Screen.sleepTimeout = SleepTimeout.NeverSleep; // 运行着的时候设备屏幕不休眠  
    
    Screen.SetResolution(1920, 1080, false); // 一般是在非移动设备上才设置分辨率

    Screen.fullScreen = true;// 运行时全屏  
    Screen.fullScreenMode = FullScreenMode.Windowed;//  独占全屏 全屏窗口 最大化窗口 窗口模式 去枚举里面看  
}

📝Note

坐标系汇总
世界坐标系-世界原点为中心
物体坐标系-建模时候确定,相对父对象的坐标
屏幕坐标系-使用Screen获得宽高原点在左下角
视口坐标系-和将屏幕坐标系百分比化,xy最大值都是1,对角为(1,1)
视口坐标和场景坐标转换API也在Camera中Camera.main.WorldToViewportPoint()

Camera和Light

Camera可以设置的属性,非核心的随便看看就行了

  • ClearFlags Skybox3D游戏常用;Soilder Color 2D游戏常用;Depth Only叠加渲染;最后是覆盖渲染模式不常用
  • CullingMask 可以选择哪些Layer会被此摄像机渲染出来(想一下tag和layer系统
  • Projection 透视模式或者正交模式
    • Perspective 透视模式
      • FOV Axis 视场角轴一般不改就用竖直方向就行
      • Field of view 视口大小
      • Physical Camera可以模拟真实世界中的摄像机焦距传感器尺寸透镜移位等,不懂摄影不改他
    • Orthographic 正交摄像机(一般用于2D游戏制作)
      • Size 摄制范围
  • ClippingPlanes 摄像机梯形盒的厚度
  • Depth 多摄像机渲染的顺序,当多个摄像机渲染多帧,深度越深越早被叠加,会被后面摄像机的帧盖住
    • 此时ClearFlags如果选择了Depth Only,那么此摄像机就只渲染选定层数的物体,然后按顺序叠加上去
    • 如果想要使用多个摄像机,往往上层的摄像机会使用此模式。(UI一般会单独使用一个摄像机去渲染。这个很重要
  • TargetTexture 渲染纹理,把摄像机输出的画面输出到一张图上,可以用来制作预览和小地图
    • 创建RenderTexture选中即可使用
  • Occlusion Culling 默认勾选被遮挡的物体不渲染省性能
  • ViewportRect 不太重要可以调出类似于双人成行的效果
  • TargetDisplay 不太重要可以输出到多个屏幕

从代码中获取摄像机的场景中Camera的Tag为MainCamera的摄像机可以直接通过静态方法Camera.main获取,如果有多个(一般只有一个)随机获得一个
Inspector中的属性都可以用对象获得,摄像机提供的委托前妻不常用
注意 屏幕坐标<->世界坐标 转换Z轴值的问题

Click to see more

摄像机的的常用API用法

void Start()  
{  
    print(Camera.main.transform.position);  
    foreach (Camera c in Camera.allCameras) print(c);// 获得场景所有摄像机的数组  
    print(Camera.allCamerasCount); //  获得摄像机的计数  
  
    // 转换之后返回Vector3, xy就是屏幕坐标,z就是物体离屏幕远近  
    Camera.main.WorldToScreenPoint(transform.position);  
    // 屏幕转世界  
    Vector3 mousePos = Input.mousePosition;  
    // 如果不改变这个值,那么屏幕上的值是没有Z的,默认就是0  
    mousePos.z = 10; // 所以这里要改成10,这里的Z就是 距离摄像机横切面是多远  
    //  这样才不会一直是一个点,如果Z是0那么xy在三维空间中转换也是一直在一个点上  
    Camera.main.ScreenToWorldPoint(mousePos);  
  
    // 在特殊的时刻调用的委托  
    Camera.onPreCull += Func; // 比如这个就是 剔除遮挡 时候的委托  
    Camera.onPreRender += Func;  
    Camera.onPostRender += Func;  
}

Light相关,一眼能看出来的和不重要的属性就不放上去了。代码改改光晕强度什么的,照着面板改吧,其实提供的API不多
可以用点光源挑动和Holo大小模拟蜡烛,用方向光模拟白天黑夜轮换等

  • Type 光源类型
    • Spot 聚光灯
      • Range 发光范围距离
      • Spot Angle 光锥角度
    • Directional 方向光(环境光)
    • Point 点光源
    • Area 面光源 只有在烘焙模式下有用不管他
  • Mode 光源模式
    • Realtime 实时光源 每帧实时计算,效果好,性能消耗大
    • Baked 烘焙光源 事先计算好,无法动态变化
    • Mixed 混合光源 预先计算+实时运算
  • RealtimeShadows 实时阴影
  • Cookie 灯罩,用灰度图叠加出来光的形状,类似于灰度遮罩那种
    • Cookie Size 灯罩大小
  • Flare 耀斑,镜头耀斑,动漫开场里太阳那种
    • 注意必须给Camera添加FlareLayer组件 才能在Game中看到耀斑
  • Draw Halo 球形光环开关
  • Culling Mask 哪些Layer会受到此光源影响

光源环境的设置在 菜单Window->Rendering->Lighting里面
在SkyboxMaterial中可以设置默认的天空盒(其实就是6个面的美术资源),默认最亮的方向光代表太阳
也可以改Fog雾(吃性能) Flare耀斑 Holo光晕 效果等,接触不到的时候暂时不用看

烘焙

Collider和Rigidbody

碰撞产生的条件: 两个物体都有Collider且至少一个有Rigidbody 有了刚体Rigidbody物体才会受到力的作用

  • 质量Mess越大越不容易被别的力推动(惯性大)。有了Rigibody才能受重力UseGravity影响,空气阻力Drag参数才有用。互相碰到的时候会产生扭矩作用力,随着飞行被AngularDrag扭矩阻力衰减,让物体旋转着飞出去。
  • IsKinematic是否直接用代码接管物理特性
  • 当物理帧更新间隔太长的时候,可以使用插值运算Interpolate让物体的物理现象更平滑
    • 菜单Edit->PorjectSettings->Time->FixedTimestep每隔多少秒进行一次物理计算,也就是FixUpdate的间隔。
    • Interpolate 根据前一帧的变换来平滑变换
    • Extrapolate 根据下一帧的估计变换来平滑变换
  • CollisionDetection碰撞检测频率算法 性能消耗依次增高,相应也更准确
    • Discrete默认频率较低,有时候速度比较快可能没检测到,就会穿模
    • Continuous连续检测
    • Continuous Speculative连续推测检
    • Continuous Dynamic连续动态检测
  • Constraints可以冻结某个轴的旋转或坐标,全冻结就失去了物理特性,比如冻结Y轴的位移可以避免对象被弹飞
使用不同检测算法的物体
互相碰撞会用什么算法
无刚体碰撞盒Discrete
(离散检测)
Continuous
(连续检测)
Continuous Dynamic
(连续动态检测)
Continuous Speculative
(连续推测检测)
无刚体碰撞盒不检测碰撞DiscreteContinuousContinuousContinuous Speculative
DiscreteDiscreteDiscreteDiscreteDiscreteContinuous Speculative
ContinuousContinuousDiscreteDiscreteContinuousContinuous Speculative
Continuous DynamicContinuousDiscreteContinuousContinuousContinuous Speculative
Continuous SpeculativeContinuous SpeculativeContinuous SpeculativeContinuous SpeculativeContinuous SpeculativeContinuous Speculative

Collider是描述物体体积的,有了Collider物体才有体积不会穿模。IsTrigger勾选会只有碰撞检测,但是没有物理效果(比如剑和魔法穿过人物)。
Rigidbody直接加在父对象上,会把子对象都包括进来,Collider也是,父对象的碰撞检测会使用所有子对象累积起来的形状参与检测
网格碰撞器开启刚体必须开启Convex才能参与刚体计算。

在代码中检测
碰撞和Trigger属于特殊的生命周期函数,在FixedUpdate之后固定调用,调用循环间隔和Update不一样,也是通过反射调用。
只要挂载的对象能和别的对象产生碰撞和触发,那么对应的函数就会响应,有物理效果的是Collison没有的是Trigger,自己没挂别的挂了碰我也会触发
默认private,可以写成protected去子类中去重写,一般不会手动调用所以不要写成public。
参与计算的是Rigibody组件,父物体挂组件子物体挂了脚本触发不了的。
Rigibody添加力的单位是牛N,如果没阻力就会一直飞,想要一直动就一直加力和物理世界一样。注意爆炸力函数只对挂载的本脚本的物体起效,所以模拟爆炸就获得所有受影响的物体然后执行这个函数
使用组件ConstantForce组件,可以直接在面板上为物体施加力场,注意Unity有刚体休眠机制节约性能,如果发现刚体不好使了,叫醒一下

Click to see more

碰撞和力的基本API用法

public class Rigid : MonoBehaviour
{
    private Rigidbody rigidBody;
    void Start()  
    {  
        rigidBody = gameObject.GetComponent<Rigidbody>(); // 获得刚体组件  
        rigidBody.AddForce(Vector3.forward*10); // 添加相对于世界坐标系方向的力  
        rigidBody.AddRelativeForce(Vector3.forward*10); // 添加相对于自己标系方向的力  
        rigidBody.AddTorque(Vector3.forward*10);// 添加扭矩力  
        rigidBody.AddRelativeTorque(Vector3.forward*10);// 添加相对于自己标系方向的扭矩力  
        rigidBody.AddExplosionForce(100,Vector3.zero, 10); // 爆炸力 爆炸中心 爆炸半径  
        
        rigidBody.AddForce(Vector3.forward*10,ForceMode.Force); // 第二个参数是施加力的模式,力的函数都可以添加
          
        rigidBody.velocity = Vector3.forward*10; // 很少用,可以直接改变速度  
    }

    private void OnCollisionEnter(Collision collision) // 刚碰撞到
    {
        print(collision.collider); // 获得对方碰撞器的信息
        print(collision.gameObject); // 获得对方游戏对象
        print(collision.transform); //  获得对方的位置信息
        print(collision.contactCount); // 获得有哪些点碰撞了
        ContactPoint[] pos = collision.contacts; //  甚至获得所有碰撞了的点(不常用
    }
    private void OnCollisionExit(Collision collision){} // 结束碰撞的
    private void OnCollisionStay(Collision collision){} //  持续碰撞过程中

    /* Collider 对象属性和Collison差不太多,点进去看着属性拿 */
    private void OnTriggerEnter(Collider other) {} // 刚开始接触
    private void OnTriggerExit(Collider other){} // 结束接触,穿越过去的时候
    private void OnTriggerStay(Collider other){} //  两者正在穿模的时候

    void Update()
    {
        if(rigidBody.IsSleeping()) rigidBody.WakeUp(); //  当然也有rigidBody.Sleep()
    }
    //  让物体位移的方式,position赋值,Translate,AddForce, 
}

#todo 力的模式和计算,CharacterController相关

范围检测和射线检测

被检测的对象必须要有碰撞器。范围检测不会真正产生新碰撞器,而且检测是瞬时的。生成的触发器形状和默认能创建的那几个物体类型一样,最后会获得一个检测到的Coillder数组

Coillder[] colliders = Physics.OverlapBox(Vector3.zero, Vector3.one, Quaternion.AngleAxis(45,Vector3.up), //  中心 大小 旋转角
    1<<LayerMask.NameToLayer("UI"), //  检测哪一层
    QueryTriggerInteraction.Ignore); //  是否忽略触发器
//  Physics.OverlapBoxNonAlloc多了个参数,直接先传入碰撞器数组存储结果,和ref一样 调用者负责初始化数组,方法只负责填充数据。

QueryTriggerInteraction是个枚举类,默认值是使用Unity中的设置Edit->ProjectSettings->Physics->QueriesHitTriggers默认是勾上的,是否检测触发器Trigger。

射线检测 也是瞬时的。
注意Physics.RaycastAll 距离从远到近排序 排序的,最远的在index0

Click to see more

射线检测主要的API

void Start()
{
    Ray r = new Ray(transform.position, transform.forward); // 从哪个点发出射线,以及射线的方向向量
    print(r.origin);
    print(r.direction);
    Ray r2 = Camera.main.ScreenPointToRay(Input.mousePosition); // 从摄像机位置发条射线到鼠标的位置
}

void Update()
{
    Ray r3 = new Ray(Vector3.zero, Vector3.forward); // 搞一条射线
    bool isHit=Physics.Raycast(r3,1000,1<<LayerMask.NameToLayer("Monster")); // 只返回是不是碰到了 也可以传QueryTriggerInteraction
    if (Physics.Raycast(Vector3.zero, Vector3.forward, 1 << LayerMask.NameToLayer("Monster")))
    {
        print($"Hited {isHit}");
    }

    RaycastHit rh;
    if (Physics.Raycast(r3,out rh,1000,1 << LayerMask.NameToLayer("Monster")))
    {
        print(rh.collider.gameObject.name); // 这样就拿到了碰到了谁,只能拿到第一个碰到的
        print(rh.distance);// 获得离我多远
        print(rh.normal); // 获得碰撞的法线向量
    }

    RaycastHit[] rhs = Physics.RaycastAll(r3, 1000, 1 << LayerMask.NameToLayer("Monster"));
    if (rhs.Length!=0)
    {
        foreach (RaycastHit r in rhs)
        {
            print(r.collider.gameObject.name);
        }
    }
}

顺口把这俩API也看一眼,这俩是调试用的射线

Debug.DrawLine(A.position, B.position, Color.red, 0.1f);// 绘制从 A 到 B 的红色线段 显示时间0.1s
Debug.DrawRay(A.position, A.up * 5, Color.green, 0.1f); // 绘制从 A 出发的绿色射线,向 A 的上方方向 显示时间0.1s

PhysicMaterial和LineRenderer

物理材质和显示的模型材质不同,物理材质规定的是碰撞时物理特性。右键创建之后可以在Collider中为对象分配。

  • Dynamic Friction 物体在上面运动时候的摩擦
  • Static Friction 推动物体开始动的摩擦
  • Bounciness 0不会反弹 1在反弹时不产生任何能量损失,但可能只会给模拟增加少量能量。
  • Friction Combine 摩擦组合方式
    • Average:对两个摩擦值求平均值
    • Minimum:使用两个值中的最小值
    • Maximum:使用两个值中的最大值
    • Multiply:两个摩擦值相乘
  • Bounce Combine 弹性组合方式与Friction Combine模式相同

使用LineRenderer组件 可用多个划一些线段,点的多少可以增加减少,他会把点连起来绘制线段,每个对象身上只能有一个LineRenderer

  • Loop 是否首尾点自动相连
  • Positions调整线段粗细曲线
  • Corner Vertices (角顶点的圆角)绘制角时使用了多少额外的顶点增大线角看起来更圆
  • End Cap Vertices (终端顶点的圆角)
  • UseWorldSpace对齐世界坐标还是本地坐标
    材质什么的要么一看就懂要么就是不太重要的 面板里的东西和代码属性一一对应点进去看就行,注意怎么代码加点
lineRenderer.positionCount = 4; //  必须要先规定个数
lineRenderer.SetPositions(new Vector3[] {
    new Vector3(0, 0, 0),
    new Vector3(0, 0, 5),
    new Vector3(5, 0, 0)
});
lineRenderer.SetPosition(0, new Vector3(0, 0, 0)); // 重载可以指定操作

音频系统

常用兼容格式mp3 wav ogg aiff面板下面能预览,参数中比较重要的是 LoadInBackground后台加载不阻塞主线程 LoadType性能和内存取舍参数 Preload Audio Data场景使用前是否预加载音效

quote

参数一览

  • Force To Mono 强制为单声道时,混合过程中被标准化 适合需要将多声道音频转换成单声道的情况
  • Load In Background 在后台加载,不会阻塞主线程,有助于提高应用的响应速度和用户体验
  • Ambisonic 立体混响声,非常适合360度视频和XR应用程序 如果音频文件包含立体混响声编码的音频,请启用此选项
  • LoadType (加载类型)
    • Decompress On Load 不压缩形式存在内存中,虽然会占用更多内存但可以快速加载 适用于小音效
    • Compress in memory 压缩形式存在于内存中,尽管加载时间较长但是能节省内存空间 仅适用于较大音效文件
    • Streaming 以流的形式存在,使用时解码 这种形式对内存的影响最小,但可能会增加CPU的消耗
  • Preload Audio Data 勾选后进入场景即加载音频数据;不勾选则在第一次使用时才加载,有助于管理首次加载时间
  • Compression Format (压缩方式)
    • PCM 音频以最高质量存储,适合对音质有高要求的应用场景
    • Vorbis 相比PCM,提供了更高效的压缩率,根据所需的质量级别调整压缩程度
    • ADPCM 包含一定的噪音,适用于会被多次播放的声音,如碰撞声或环境音效
  • Quality (音频质量) 确定要应用于压缩剪辑的压缩量 不适用于 PCM/ADPCM/HEVAG 格式
  • Sample Rate Setting (采样率设置) PCM 和 ADPCM 压缩格式允许自动优化或手动降低采样率
    • Preserve Sample Rate 此设置可保持采样率不变(默认值)
    • Optimize Sample Rate 此设置根据分析的最高频率内容自动优化采样率
    • Override Sample Rate 此设置允许手动覆盖采样率,因此可有效地将其用于丢弃频率内容

AudioSource组件是让 GameObject发出声音的组件。AudioListener是声音监听器,可以理解为声音总线,一般会跟随摄像机挂载有且仅有一个多了删掉,如果出现了多个AudioListener会报警告。 注意参数:

  1. Play On Awake (启动播放开关):物体Awake事件时立即播放
  2. Spatial Blend (空间混合) 2D 音源(0)例如给UI用固定、3D 音源(1)随GameObject有远近,或者是二者插值的复合音源。一般只会01
  3. Volume Rolloff 声音随距离衰减速度,选择不同的模式看曲线就知道了,3D的游戏线性比较真实

quote

参数一览

  • AudioClip (音频剪辑):播放的音频
  • Output 默认将直接输出到音频监听器(默认为MainCamera挂的)可以更改为输出到混音器
  • Mute 静音开关(静音了不是不播放了)占用内存小可以放很多音效,因此可以让画面快速响应关闭和恢复当前音效
  • Bypass Effect 开关滤波器效果
  • Bypass Listener Effects 快速开关所有监听器
  • Bypass Reverb Zones 快速开关所有混响区
  • Play On Awake 对象创建时就播放音乐,也就是开关启动游戏就播放
  • Loop 循环
  • Priority 优先级
  • Volume 音量大小
  • Pitch 音高
  • Stereo Pan 调节左右声道
  • Spatial Blend 音频受3D空间的影响程度
  • Reverb Zone Mix 到混响区的输出信号量
  • 3D Sound Settings 和 Spatial Blend 参数成正比应用
    • Doppler Level 多普勒效果等级
    • Spread 扩散角度设置为3D立体声还是多声道
    • Volume Rolloff 声音衰减速度
      • Logarithmic Rolloff 靠近音频源时,声音很大,但离开对象时,声音降低得非常快
      • Linear Rolloff 与音频源的距离越远,听到的声音越小
      • Custom Rolloff 音频源的音频效果是根据曲线图的设置变化的
    • Min/Max Distance 最小距离内,声音保持最大响度;最大距离外,声音开始减弱

使用代码控制音频
没开始Play是不能Pause的。动态控制音频常用的方式,可以直接搞好挂对象身上;可以挂在一个prefab上然后动态生成销毁;可以通过指定clip用一个AudioSource组件管理。
PlayOneShot 允许你在一个已有的 AudioSource 组件上播放一次AudioClip,而不会影响当前正在播放的音频,例如播放背景音乐的同时播放角色行走或射击的音效
AudioClip类要用浮点数组存储,长度是 声道数x剪辑长度 可以Get和Set数据
麦克风输入音频 frequency默认就填44100,返回的是一个AudioClip

Warning

一个AudioSource管一个音效,如果使用同一个, 那PlayOneShot和Play可能会会被一块触发导致重叠播放,比如上面想用按键控制攻击音效的时候,背景音乐同时也会跟着起停。所以改为了获取两个组件(问题来了我怎么知道我获取的组件顺序哪个是BGM哪个是攻击)

Click to see more

音频组件的基本API

public class AusT : MonoBehaviour  
{
    private AudioSource aus;
    public AudioClip clip;
    public AudioClip RecordClip;
    // Start is called before the first frame update
    void Start()
    {
        aus=GetComponent<AudioSource>();

        string[] microps=Microphone.devices; //  所有的麦克风设备
        foreach (string microp in microps)
        {
            print(microp);
        }

    }

    // Update is called once per frame
    void Update()
    {
        if(Input.GetKeyDown(KeyCode.P)) aus.Play();
        if(Input.GetKeyDown(KeyCode.S)) aus.Stop();
        if(Input.GetKeyDown(KeyCode.Space))
        {   // 实际上Unity没有提供音效是否播放完毕的API,用isPlaying一直检测,直到播完
            if(aus.isPlaying) aus.Pause(); // 暂停再开始会接着播
            else aus.UnPause(); //  暂停之后用也行
        }
        if(Input.GetKeyDown(KeyCode.C)) aus.clip=clip; // 更换clip

        if (Input.GetKeyDown(KeyCode.R))
        {
            // 设备使用默认选null loop如果录制超长会重录 录制长度(S) 采样率一般就这个固定数44100
            RecordClip= Microphone.Start(null, false, 10, 44100);
        }

        if (Input.GetKeyUp(KeyCode.R))
        {
            print(Microphone.IsRecording(null)); // 录制完了吗
            Microphone.End(null); // 结束
            aus.PlayOneShot(RecordClip); // 不知道为什么我这里能录制,能调用麦克风 但播放会有问题
            float[] f = new float[RecordClip.channels*RecordClip.samples]; // 浮点数组存储,存储的长度是 声道数*剪辑长度  
            RecordClip.GetData(f, 0); // 直接Get出来塞进去
        }
    }
}

动画系统

精灵Sprite


游戏管理

委托

#todo 这里以后要再学一学,C#中的委托和Unity中委托的异同点
Unity 额外提供了 UnityEvent,它是 System.Delegate 的封装,主要用于与 Inspector(检查器)交互,支持序列化,可以在 Unity 编辑器中直接绑定回调 Unity 的事件生命周期管理 Unity 提供了 Start(), Update(), OnDestroy() 等生命周期方法,而 C# 传统的委托需要手动管理生命周期。GC(垃圾回收)影响 Unity 的 UnityEvent 是基于 List<Delegate> 而非 C# 的 MulticastDelegate,因此不会像 C# 事件那样造成隐式的 GC 压力(捕获 this 时的内存泄漏风险较低)

📝Note

柯里化概念复习
柯里化(Currying)是一种将接受多个参数的函数转换成接受一个单一参数的函数,并且返回接受余下参数的函数的技术。这种技术由逻辑学家Haskell Curry提出,因此得名柯里化。
假设有一个函数 f(a, b, c),经过柯里化处理后,它会被转换成 f(a)(b)(c) 的形式。
反柯里化(Uncurrying)是柯里化的逆操作,它将嵌套的函数转换为一个接受多个参数的函数。反柯里化可以使得嵌套的单参数函数变得更易用,特别是在需要处理多个参数的情况下。
假设有一个柯里化函数 f(a)(b)(c),经过反柯里化处理后,它会被转换成 f(a, b, c) 的形式。

线程和协程

线程其实正常的起就可以,一般是会把密集的计算或异步IO,线程没法直接传递结果,一般会声明公共的缓冲区,由untiy周期函数尝试去拿,拿到了就是计算完了。
其实就可以直接搞生产消费者那套了

Warning

线程一定要注意
千万别Join,Unity编辑器本质也是通过反射调用的C#,如果在生命周期里Join整个程序会卡住。
所以线程用完一定要关闭!!!
子线程无法访问Unity住线程内容中的东西

Click to see more

简单多线程演示

void Start()  
{
    Thread t = new Thread(() => TestThread());  
    t.Start();  
}
private void TestThread()  
{  
    int count = 0;  
    while (true)  
    {        count += 1;  
        Thread.Sleep(1000);  
        Debug.Log($"TestThread print {count}");  
        this.GetComponent<Collider>();// 这里尝试访问住线程的对象报错了!  
    }  
}

Unity中的协程通常在一段时间内执行某些任务或在帧之间分配工作。协程通过 IEnumerator 接口和 StartCoroutine 方法来实现。

  • 返回值必须为IEnumerator接口类型,使用yield return
  • 是非阻塞的,适合需要在多个帧之间执行的任务。
  • 依赖于Unity的更新循环,无法脱离Unity环境使用。
  • 脚本 物体销毁,物体失活 协程不执行。脚本如果已经运行了协程然后再失活协程仍会执行。
Click to see more

协程测试

void Start()  
{        
    Coroutine cor=StartCoroutine(AsyncDemo(10)); // 去执行一个协程  
    StopCoroutine(cor); // 结束此协程调用  
    StopAllCoroutines(); // 停止掉此脚本此脚本此脚本协程池里面所有协程  
    // StartCoroutine(AsyncDemo(10)); // 也可以这样,但是这样不知道在哪里用了几次。
    // 所以建议直接不要这样用,做一件事最好的方法有且仅有一个  
}
public IEnumerator AsyncDemo(int num)  //  协程的固定返回类型  
{  
    Debug.Log("wait for return ");  
    yield return new WaitForSeconds(3f); // 3s之后返回,有很多不同的返回函数,用到再看  
    Debug.Log("done return 1s");  
    yield return null;  
    Debug.Log($"done next return , out {num}");  
    transform.position = new Vector3(10, 10, 10); // 做一次位移  
    yield return AsyncDemo2();  //  协程是可以嵌套的  
}  
public IEnumerator AsyncDemo2() // 利用协程做动画,以及协程嵌套,协程本质就是一个函数栈暂停,控制权移交  
{  
    Debug.Log("start async2");  
    while (true)  
    {            
        transform.Rotate(new Vector3(10,0,0));  //  调整角度,配合协程达到旋转的效果,不过这个旋转不是平滑的  
        // 这里可以用while true执行
        yield return new WaitForSeconds(0.5f); // 0.5秒返回一次  
    }  
} 

运行结果

[22:40:01] wait for return
[22:40:04] done return 1s
[22:40:04] done next return, out 10
[22:40:04] start async2

yield返回的特殊方法

  • yield return null/数字yield return WaitForSeconds:在满足条件后的下一帧 UpdateLateUpdate 之间执行
  • yield return WaitForFixedUpdate:在 FixedUpdate 和碰撞检测相关函数之后执行(物理更新后)
  • yield return WaitForEndOfFrame:在摄像机和GUI渲染完成后FixedUpdate 之后执行
  • yield break:立即结束协程,协程对象会变null
  • 其他的异步对象(网络 场景)一般是UpdateLateUpdate 之间执行
    协程的本质是迭代器,然后Unity调度器执行,和py里面迭代器一样,使用Current和MoveNext执行,直接点进IEnumerator看。

延时函数

MonoBehaviuor中提供了延迟调用函数的方法Invoke InvokeRepeating CancelInvoke (参数都简单易懂
延时执行只能无参,而且只能执行本脚本中的对象,但是可以先获得,再通过函数或者Action包一下执行。
Invoke即为调用,Invoke多次会调用多次,但是CancelInvoke是按照函数签名去取消的,也就是说会把该函数的调用全都踢出队列取消掉。
注意 对象或脚本失活不会影响延迟函数调用;但是对象或脚本销毁移除,就延迟函数不会执行了 如果想要用失活激活控制,可以放到Enable和Disable中控制

Click to see more

延时函数

void CurryingDelyDemo(string s)=>Debug.Log($"be Invoked {s}");
public void InvokeDemo()=>Debug.Log("be Invoked");
public void CalcelInvokeDemo()
{
    Debug.Log("Invoke is Canceled");
    CancelInvoke("InvokeDemo");
}  
void Start() // GameObject加载完毕后所有的Awake执行完,的第一帧执行一次  
{  
    Action myInvokeDemo = () => CurryingDelyDemo("Hello, World!");  
    Debug.Log("Object start successfully Complete");  
    // 定时任务  
    Invoke("InvokeDemo",3f); // 3秒(浮点)后通过反射来执行函数  
    InvokeRepeating("InvokeDemo",2f,3f); // 从调用的2秒后每3s执行一次  
    Invoke("CalcelInvokeDemo",10f); //10秒(浮点)后通过反射来执行取消  
    Invoke(myInvokeDemo.Method.Name,3f); // 通过委托的闭包传递参数  
}  

资源

特殊文件夹

  1. Application.dataPath是总的工程目录文件夹Assets 的路径,在构建之后并不直接暴露给应用程序不能直接访问,一般只在开发阶段用用,也不保证跨平台一致性
  2. Resources资源文件夹直接使用Resources类读取资源。Resources可以在任意层级任意地方,但必须在Assets文件夹下,多个Resources会被合并起来。
    • 当不同Resources中文件重名,会加载第一个找到的文件,如果有文件夹重名,则会报警告提示重复的资源路径 。在Start中使用prefab_cube = Resources.Load<GameObject>("Perfabs/s2");即可加载(非实例化仅加载对象到内存)。
    • 适用于需要经常在运行时加载并实例化的资源(如场景、预制件、材质等)Unity会自动打包引入的资源文件,但是这个文件夹里的会全都加载。
    • 不要存配置文件、日志文件等需要原样读取的文件,因为 Unity 会自动处理和打包,打包完后只读。
  3. 位于Assets/StreamingAssets,使用Application.streamingAssetsPath;获得
    • 移动平台只读PC可读写
    • 常用string path = Path.Combine(Application.streamingAssetsPath, "netConfig.json");读取其中资源
    • 跨平台一致性 文件夹中的内容在所有平台(Windows、macOS、iOS、Android等)上保持一致
    • 文件保持原样 Unity 不会对 StreamingAssets 中的文件进行压缩或优化,适合存放媒体、配置文件、游戏内语言包、地图数据等静态数据
  4. persistentDataPath持久数据文件夹Application.persistentDataPath获得使用在运行时可读写,动态下载或者游戏中生成的东西存储
  5. Plugins插件文件夹 放第三方SDK的
  6. Editor编辑器文件夹 使用Application.dataPath+"/Editor"拼接,一般不获得;开发编辑器的时候脚本放里面;不会被打包
  7. Standard Assets默认资源文件夹 Unity自带的文件都放这个文件夹,代码和资源优先被编译,一般不获得。

Warning

注意文件名
资源文件不要带文件后缀名,最好也不要带特殊字符,而且文件名是大小写敏感的

使用 Resources.Load<T>("Name",[typeof(TextAsset)]) 同步加载资源,也可以指定类型或者传泛型。默认加载出来的类型为Unity.Object 可能需要类型转换
常见类型

  • 预设体GameObject
  • 音效AudioClip
  • 文本TextAsset
  • 图片Texture
  • 材质Material
    Resources加载到内存后会存在缓存区,多次加载不会重复加载,只会重复在缓存区中查找也会损耗一点性能。
    异步加载Resources.LoadAsync会新起一个线程进行加载,至少要等到下一帧才能拿到结果。
Click to see more

异步加载和使用协程异步加载

Texture TargetTexture;  
void Start()  
{  
    /* 使用线程加载 */    
    // ResourceRequest Rq = Resources.LoadAsync<Texture>("Picture/t"); // 该类型包含Action对象  
    // Rq.completed += LoadDone; // 加载完之后通过回调通知执行  
    /* 使用协程加载 */   
    Coroutine cor=StartCoroutine(LoadTexture());  
}  
  
private void LoadDone(AsyncOperation op)  
{  
    TargetTexture = (op as ResourceRequest).asset as Texture;  
    print("Loading Done");  
}  
  
IEnumerator LoadTexture()  
{  
    ResourceRequest Rq = Resources.LoadAsync<Texture>("Picture/t");  
    yield return Rq; //  协程调度器会监控 Rq.isDone 的状态 Rq.priority,完事才执行下面的  
    while (!Rq.isDone) //  也可以手动监控  
    {
        print($"porcessing {Rq.priority}");  
        yield return Rq;  
    }    TargetTexture = Rq.asset as Texture;  
}  

public void OnGUI()  
{  
    if (TargetTexture != null)  
    {        print("Draw texture");  
        GUI.DrawTexture(new Rect(0,0,200,100), TargetTexture);  
    }  
}

资源必须是通过 Resources.Load 加载的才能卸载,使用Resources.UnloadAsset(oldTexture);卸载,但是不能卸载GameObject对象即使没有实例化。
使用Resources.UnloadUnusedAssets 卸载所有未被引用的资源,此操作是异步的,会遍历所有资源并检查引用计数性能消耗很高,一般搭配GC.Collect()在过场景的时候使用。 某个资源正在被场景中的对象使用(例如一个纹理正被某个材质使用),卸载它可能导致渲染异常或崩溃

加载场景

场景使用静态类UnityEngine.SceneManagement.SceneManager,需要引入命名空间。能够直接加载的场景需要提前添加到 菜单->File->BuildSettings->Scenes in Build 中,注意这个里面场景有序号。
然后就可以直接SceneManager.LoadScene("NextScene") 加载场景,一般最常用的就是通过名字加载,GameObject.DontDestroyOnLoad(gameObject)挂载者自己在场景切换的时候不销毁
退出游戏使用Application.Quit(),打包发布出去才行,编辑模式下没用。(Application类可以获得很多和游戏相关的属性,比如游戏是否在运行isPlaying
加载的时候Unity会删除本场景上的所有对象然后读取下一个场景的东西,东西比较多的时候就耗时多。
异步加载场景SceneManager.LoadSceneAsync("NextScene")返回值和资源异步加载一样是AsyncOperation,可以使用complete事件绑定回调。常用来加载空场景然后使用指定的地图编辑器配置文件生成新场景的内容

Click to see more

异步加载场景的两种方法

void Start()
{
    AsyncOperation loading=SceneManager.LoadSceneAsync(1);
    /* 注意这里,理论上这里加载完后就销毁,但是这个函数被回调引用,引用计数不会销毁它,所以能继续执行 */
    loading.completed += (AsyncOperation a) => print("场景2加载完毕");
}

IEnumerator LoadScene2()
{
    print("Loading Scene 2");
    yield return SceneManager.LoadSceneAsync(1);
    /* 这样下面的语句就会执行,否则场景销毁对象也已经被销毁这里就不会被执行。 */
    GameObject.DontDestroyOnLoad(this.gameObject);
    print("Loading Scene 2 done");
    /* 这里就可以加过渡和进度条等,按照你想的进度去做 */
}
private float time = 0;
void Update()
{
    time += Time.deltaTime;
    if (time > 2)
    {
        StartCoroutine(LoadScene2());
    }
}

鼠标

鼠标使用公共类Cursor管理,如果你不想鼠标变形就找个一样大小的图片,最好把这个素材的Inspector面板的类型设置为Cursor

Cursor.visible = false; // 显不显示  
Cursor.lockState = CursorLockMode.Locked; // None不限制,Locked锁在屏幕中心还会被隐藏,Confined缩在游戏窗口  
Cursor.SetCursor(cursor_texture, Vector2.zero, CursorMode.Auto); //  一个Texture2d 偏移量 自动管理。

持久化

PlayerPrefs

持久话键值对,{string:[int string float]} 默认是游戏结束才落盘,如果崩溃就丢了,其实就是个类型支持不全的字典。
存到哪,在BuildSettings->PlayerSettings->CompanyName;ProductName设置。

  1. win在注册表 \HKCU\Software\[CompanyName]\[ProductName]
  2. 安卓/data/data/包名/shared_prefs/pkg-name.xml
  3. IOS /Library/Preferences/[应用ID].plist
Click to see more

PlayerPrefs的基本API

void Start()  
{  
    PlayerPrefs.SetInt("Test1", 10);  
    PlayerPrefs.SetFloat("Test2",0.5f);  
    PlayerPrefs.SetString("Test3","ttttt");  
    PlayerPrefs.Save();// 落盘  
    PlayerPrefs.DeleteKey("Test1");  
    //  如果读取的时候对应的类型错误 或者没有这个键,则会返回读取类型的默认值  
    print(PlayerPrefs.GetInt("Test1", 1)); //  get的时候和dict一样可以获取默认值  
    print(PlayerPrefs.GetFloat("Test2",1f));  
    print(PlayerPrefs.GetString("Test3","NoValue"));  
  
    PlayerPrefs.DeleteAll(); // 全清除,测试常用  
    if(PlayerPrefs.HasKey("Test1"))print("Test1 有东西!");  
}

其他持久化方式

这几个不是Unity自带的,C#直接看的书,有关序列化的坑也在这里了。

JSON

主流的这三个newtonsoft.json, fastjson , jsondotnet,只看第一个就行剩下俩碰到再说

Protobuf

XML

XmlDocument XmlTextReader Linq常用的三种,一般无脑用XmlDocument和Linq。

Binary

读写Excel

其实就是一种文件格式,没什么新奇的,我用pandas不是天天干这个吗


打包设置

关注Resolution and Presentation

  • Fullscreen Mode
    • FullscreenWindow使用显示器的分辨率进行全屏显示,如果游戏分辨率低于显示器分辨率,可能会出现黑边
    • Exclusive Fullscreen 修改显示器分辨率以匹配项目,全屏
    • Maximized Window 最大化窗口
    • Windowed窗口模式
  • Resolution
    • Default Is Native Resolution 使用显示器分辨率,不勾使用设置的分辨率 Windowed模式才有用
    • Default Screen Height 游戏画面默认高度,Windowed模式才有用
    • Mac Retina Support启用MacOS高DPI支持
    • Run In background程序失去焦点可以继续运行而不暂停

判断是否进入了某个点和简单版多点之间巡逻

this.transform.LookAt(tragetPos);  
this.transform.Translate(Vector3.forward * (this.moveSpeed * Time.deltaTime));  
// 注意这个地方,移动是按照帧来算的,所以判断物体是否到了这个点要用比较小距离而不是直接position相等,否则这一帧如果直接穿过了,就无法判断了
if (Vector3.Distance(transform.position, tragetPos.position) <= 0.05f) 
{  
    print(Vector3.Distance(transform.position, tragetPos.position));  
    RandomPos(); //  因为这里每帧都会切换,所以这里即便切换到同一个也不要紧。如果距离范围太大了,反而会频繁切换。  
}


Debug.DrawLine(A.position,B.position,Color.red); // 划线段从一个点到另一个点
Debug.DrawRay(A.position,B.position,Color.white); // 划射线从起点到某个方向

如果我初始化一个子弹,速度很快,那么岂不是检测子弹重叠的范围就会很大么,不会容易子弹穿模或者性能消耗高

资源或者全局的管理等,一般初始化动作放在awake中做,而不是start,如果把这些逻辑放在 Start 中,可能会导致其他脚本在 Start 中访问 BGMusic.Instance 时,instance 还没有被赋值,从而引发空引用异常。



Last Edit: 2025-04-20 23:38:51
Loading...