目录
这周跟着代码随想录在力扣上写了些贪心题目
什么是万向节死锁
万向节死锁是指在使用三个欧拉角表示旋转时,如果其中一个旋转使得另外两个旋转轴对齐,那么就会失去一个自由度,导致无法表示出原来可以通过三个独立旋转实现的某些旋转状态。简而言之,就是在特定的旋转状态下,我们无法使用欧拉角独立控制物体的所有三个旋转方向,好像物体被锁在了某个旋转平面
四元数概念
公式:Q = [cos(β/2), sin(β/2)x, sin(β/2)y, sin(β/2)z] //β是你要旋转的角度
初始化四元数的两种方式->
不常用初始化->
Quaternion q = new Quaternion(Mathf.Sin(30 * Mathf.Deg2Rad), 0, 0, Mathf.Cos(30 * Mathf.Deg2Rad));
//创建一个立方体
GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
obj.transform.rotation = q;
轴角对是什么
是一种表示3D空间中物体旋转的方式。这种表示方法通过一个旋转轴和一个旋转角度来定义旋转。轴角对的概念基于旋转轴定理,即任何3D旋转都可以通过绕某一固定轴的单一旋转来实现。
不仅仅局限于xyz轴
常用的初始化四元数→
unity中提供的轴角对 初始化 四元数的方法
Quaternion q = Quaternion.AngleAxis(60, Vector3.right);
四元数和欧拉角之间的转换→
//1.欧拉角转四元数 Quaternion q2 = Quaternion.Euler(60, 0, 0); GameObject obj2 = GameObject.CreatePrimitive(PrimitiveType.Cube); obj2.transform.rotation = q2;
//2.四元数转欧拉角 print(q2.eulerAngles);
四元数解决了欧拉角的两大缺点——>
1.同一旋转的表示不唯一 四元数旋转后 转换后的欧拉角 始终是 -180~180度
2.万向节死锁 通过四元数旋转对象可以避免万向节死锁
四元数中的常用方法
1.单位四元数
旋转角度为0的时候,且标量w为1时的四元数我们称之为单位四元数Quaternion.identity
单位四元数常常用来初始化一个物体的旋转角度,如→在实例化物体的时候
Instantiate(testObj, Vector3.zero, Quaternion.identity);
2.Quaternion中的插值运算
在四元数中Slerp比lerp效果更好
//无限接近 先快后慢 A.transform.rotation = Quaternion.Slerp(A.transform.rotation, target.rotation, Time.deltaTime);
//匀速变化 time>=1到达目标 time += Time.deltaTime; B.transform.rotation = Quaternion.Slerp(start, target.rotation, time);
3.LookRotation
Quaternion中的这个方法传入一个向量,如果把这个四元数赋值给一个对象的rotation,他就会看向向量所指的方向,类似于transform中的LookAt
返回值是一个四元数
Quaternion q = Quaternion.LookRotation(lookB.position - lookA.position); lookA.rotation = q;
A物体就会看向B物体
所以我们可以结合LookRotation和Slerp实现摄像机在物体移动后缓慢转动跟随着物体移动,而不是瞬间锁死
即把LookRotation的返回值作为Slerp的终点rotation
例子如下→
//用目标的位置 减去 摄像机的位置 得到新的面朝向向量 targetQ = Quaternion.LookRotation(target.position - this.transform.position);
//先快后慢 //this.transform.rotation = Quaternion.Slerp(this.transform.rotation, targetQ, Time.deltaTime* roundSpeed);
//匀速旋转 if( targetQ != Quaternion.LookRotation(target.position - this.transform.position)) {
targetQ = Quaternion.LookRotation(target.position - this.transform.position);
roundTime = 0;
startQ = this.transform.rotation;
}
roundTime += Time.deltaTime;
this.transform.rotation = Quaternion.Slerp(startQ, targetQ, roundTime * roundSpeed);
四元数的计算
1.四元数相乘
Quaternion q = Quaternion.AngleAxis(20, Vector3.up);
this.transform.rotation *= q;
//第一次相乘以后,这里的Vector3.up是相对自身坐标系的,所以绕自身的y轴旋转了20°
this.transform.rotation *= q;
//再次旋转20°
2.四元数乘以向量
Vector3 v = Vector3.forward; v = Quaternion.AngleAxis(45, Vector3.up) * v;
v向量绕自身y轴旋转45° v = Quaternion.AngleAxis(45, Vector3.up) * v;’
四元数乘以向量的返回值是一个旋转向量,特别需要注意的是,四元数乘以向量,四元数必须放前面,不然的话就会报错
也就是说当我们得到了一个物体的面朝向向量的时候,我们就可以以该向量为起点,创建不同的实例化对象并为其设置不同方向的初值,类似于一些拐弯的射击啥的
延迟函数Invoke
Monobehavior中已经为我们实现好的延迟函数——>
Invoke 、InvokeRepeating
Invoke("函数名",延迟时间);
延迟函数所调用的函数必须是该脚本上声明的函数
调用延迟函数没办法传入参数,只能包裹一层函数再去调用有参数的函数
InvokeRepeating("函数名",第一次执行的延迟时间 , 往后每一次执行的延迟时间);
取消该脚本上的所有延迟函数→CancelInvoke();
根据名字取消延迟函数——>CancelInvoke("函数名");
判断该脚本上是否有延迟函数(不常用)→IsInvoking()
对象销毁或失活对脚本的影响——>
脚本所依附的对象失活或是脚本本身失活——>延迟函数仍然可以继续执行
但脚本组件被移除或者是对象被销毁——>无法继续执行延迟函数
所以要想在一个对象被失活的时候取消延迟函数可以选择在OnDisable中调用CancelInvoke();
因为OnDisable是在对象失活的前一帧被调用的
同理,可以在对象被激活的时候在OnEnable()中启用延迟函数
协程的使用
继承MonoBehavior的类 都可以开启 协程函数
第一步:申明协程函数
协程函数2个关键点
1. 返回值为IEnumerator类型及其子类
2.函数中通过 yield return 返回值; 进行返回
第二步:开启协程函数
协程函数 是不能够 直接这样去执行的!!!!!!!
这样执行没有任何效果
MyCoroutine(1, "123");
常用开启方式
IEnumerator ie = MyCoroutine(1, "123");
StartCoroutine(ie);
Coroutine c1 = StartCoroutine(MyCoroutine(1, "123"));
Coroutine c2 = StartCoroutine(MyCoroutine(1, "123"));
Coroutine c3 = StartCoroutine(MyCoroutine(1, "123"));
第三步:关闭协程
关闭所有协程
StopAllCoroutines();
关闭指定协程
StopCoroutine(c1);
yield return 不同内容的含义
1.下一帧执行
yield return 数字;
yield return null;
在Update和LateUpdate之间执行 (在Update执行完后,LateUpdate执行之前)
2.等待指定秒后执行
yield return new WaitForSeconds(秒);
在Update和LateUpdate之间执行
3.等待下一个固定物理帧更新时执行
yield return new WaitForFixedUpdate();
在FixedUpdate和碰撞检测相关函数之后执行
4.等待摄像机和GUI渲染完成后执行
yield return new WaitForEndOfFrame();
在LateUpdate之后的渲染相关处理完毕后之后
5.一些特殊类型的对象 比如异步加载相关函数返回的对象
之后讲解 异步加载资源 异步加载场景 网络加载时再讲解
一般在Update和LateUpdate之间执行
6.跳出协程
yield break;
协程受对象和组件失活销毁的影响
协程开启后 组件和物体销毁,协程不执行 物体失活协程不执行,组件失活协程执行
Resources资源同步加载
1.Resources资源动态加载的作用
1.通过代码动态加载Resources文件夹下指定路径资源
2.避免繁琐的拖曳操作
2.常用的一些资源类型
1.预设体对象——GameObject
2.音效文件——AudioClip
3.文本文件——TextAsset
4.图片文件——Texture
5.其它类型——需要什么用什么类型
注意:
预设体对象加载需要实例化
其它资源加载一般直接用
3.资源同步加载的普通方法→
//在一个工程当中 Resources文件夹 可以有多个 通过API加载时 它会自己去这些同名的Resources文件夹中去找资源
//打包时 Resources文件夹 里的内容 都会打包在一起
//1.预设体对象 想要创建在场景上 记住实例化
// 第一步:要去加载预设体的资源文件(本质上 就是加载 配置数据 在内存中)
Object obj = Resources.Load("Cube");
//第二步:如果想要在场景上 创建预设体 一定是加载配置文件过后 然后实例化
Instantiate(obj);
// 第一步:要去加载预设体的资源文件(本质上 就是加载 配置数据 在内存中)
Object obj2 = Resources.Load("Sphere");
//第二步:如果想要在场景上 创建预设体 一定是加载配置文件过后 然后实例化
Instantiate(obj2);
切记返回值是万物之父
//2.音效资源
//第一步:就是加载数据
Object obj3 = Resources.Load("Music/BKMusic"); //在Resources文件夹中创建了一个Music文件夹
//第二步:使用数据 我们不需要实例化 音效切片 我们只需要把数据 赋值到正确的脚本上即可
//public AudioSource audioS; //只要创建一个空物体,上面挂一个AudioSource组件,然后拖过来就行
audioS.clip = obj3 as AudioClip;
audioS.Play();
//3.文本资源
//文本资源支持的格式
//.txt
//.xml
//.bytes
//.json
//.html
//.csv
//.....
TextAsset ta = Resources.Load("Txt/Test") as TextAsset;
//既然我们已经知道了我们要动态加载什么样的文件,所以可以直接as
//文本内容
print(ta.text);
//字节数据组
//print(ta.bytes);
//4.图片
//tex = Resources.Load("Tex/TestJPG") as Texture;
//5.其它类型 需要什么类型 就用什么类型就行
//6.问题:资源同名怎么办
//Resources.Load加载同名资源时 无法准确加载出你想要的内容
//可以使用另外的API(重载)
//6-1加载指定类型的资源
//tex = Resources.Load("Tex/TestJPG", ) astypeof(Texture) Texture;
ta = Resources.Load("Tex/TestJPG", typeof(TextAsset)) as TextAsset;
//print(ta.text);
但对于分辨同名资源一般还是用泛型更加的方便
//6-2加载指定名字的所有资源
Object[] objs = Resources.LoadAll("Tex/TestJPG");
foreach (Object item in objs)
{
if (item is Texture)
{
}
else if(item is TextAsset)
{
}
}
4.加载指定同名类型文件的泛型方法
TextAsset ta2 = Resources.Load("Tex/TestJPG"); print(ta2.text);
tex = Resources.Load("Tex/TestJPG");
Resources异步资源加载
方法1
1.通过异步加载中的完成事件监听 使用加载的资源
这句代码 你可以理解 Unity 在内部 就会去开一个线程进行资源下载
ResourceRequest rq = Resources.LoadAsync<Texture>("Tex/TestJPG");
马上进行一个 资源下载结束 的一个事件函数监听
rq.completed += LoadOver; //事件函数必须有一个AsyncOperation类型的参数
private void LoadOver( AsyncOperation rq)
{
print("加载结束");
//asset 是资源对象 加载完毕过后 就能够得到它,该属性(asset)是在ResourceRequest中的
tex = (rq as ResourceRequest).asset as Texture;
}
方法2
通过协程方式
void Start(){
StartCoroutine(Load());
}
IEnumerator Load()
{
//迭代器函数 当遇到yield return时 就会 停止执行之后的代码
//然后 协程协调器 通过得到 返回的值 去判断 下一次执行后面的步骤 将会是何时
ResourceRequest rq = Resources.LoadAsync<Texture>("Tex/TestJPG");
print(Time.frameCount);
//第一部分
//Unity 自己知道 该返回值 意味着你在异步加载资源
//yield return rq;
//Unity 会自己判断 该资源是否加载完毕了 加载完毕过后 才会继续执行后面的代码
print(Time.frameCount);
}
//判断资源是否加载结束
while(!rq.isDone)
{
//打印当前的 加载进度
//该进度 不会特别准确 过渡也不是特别明显
print(rq.progress);
yield return null;
}
tex = rq.asset as Texture;
//yield return null;
第二部分
//yield return new WaitForSeconds(2f);
第三部分
///
如何对异步加载的资源进行使用
1. 在OnGUI
方法中使用
确实,你可以在OnGUI
方法中使用Resources.Load
来动态加载图片,并通过GUI函数(如GUI.DrawTexture
)来绘制这些图片。这种方式主要用于绘制简单的UI元素或进行快速的原型设计。
void OnGUI() { Texture2D texture = Resources.Load("path/to/your/image"); GUI.DrawTexture(new Rect(10, 10, 100, 100), texture); }
2. 通过SpriteRenderer组件使用
如果你是在2D游戏中,可以将加载的图片作为Sprite
使用,并通过更改GameObject
的SpriteRenderer
组件的sprite
属性来显示这个图片。
void Start() { Sprite sprite = Resources.Load("path/to/your/sprite");
GetComponent().sprite = sprite; }
3. 在3D对象上使用
对于3D游戏,可以将图片作为纹理动态加载并应用到3D模型的材质上。这通过修改GameObject
的Material
的Texture
属性实现。
void Start() { Texture2D texture = Resources.Load("path/to/your/texture"); GetComponent().material.mainTexture = texture; }
4. 在UI Image组件中使用
如果你使用的是Unity的UGUI系统,可以将动态加载的图片设置给Image
组件的Sprite
属性。首先需要将加载的图片转换为Sprite
,然后赋值给UI组件。
void Start() { Texture2D texture = Resources.Load("path/to/your/image");
Sprite sprite = Sprite.Create(texture, new Rect(0.0f, 0.0f, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100.0f);
GetComponent().sprite = sprite; }
场景异步切换
方法1
1.通过事件回调函数 异步加载
AsyncOperation ao = SceneManager.LoadSceneAsync("Lesson20Test");
当场景异步加载结束后 就会自动调用该事件函数 我们如果希望在加载结束后 做一些事情 那么久可以在该函数中
写处理逻辑
ao.completed += (a) =>
{
print("加载结束");
};
ao.completed += LoadOver;
private void LoadOver(AsyncOperation ao)
{
print("LoadOver");
}
方法2
通过协程异步加载
需要注意的是 加载场景会把当前场景上 没有特别处理的对象 都删除了
所以 协程中的部分逻辑 可能是执行不了的
解决思路
让处理场景加载的脚本依附的对象 过场景时 不被移除
该脚本依附的对象 过场景时 不会被 移除
DontDestroyOnLoad(this.gameObject); //防止脚本过场景被移除
StartCoroutine(LoadScene("Lesson20Test")); //启动协程
IEnumerator LoadScene(string name)
{
//第一步
//异步加载场景
AsyncOperation ao = SceneManager.LoadSceneAsync(name);
//Unity内部的 协程协调器 发现是异步加载类型的返回对象 那么就会等待
//等待异步加载结束后 才会继续执行 迭代器函数中后面的步骤
print("异步加载过程中 打印的信息");
//协程的好处 是异步加载场景时 我可以在加载的同时 做一些别的逻辑
yield return ao;
//第二步
print("异步加载结束后 打印的信息");
//比如 我们可以在异步加载过程中 去更新进度条
//第一种 就是利用 场景异步加载 的进度 去更新 但是 不是特别准确 一般也不会直接用
//while(!ao.isDone)
//{
// print(ao.progress);
// yield return null;
//}
//离开循环后 就会认为场景加载结束
//可以把进度条顶满 然后 隐藏进度条
//第二种 就是根据你游戏的规则 自己定义 进度条变化的条件
yield return ao;
//场景加载结束 更新20%进度
//接着去加载场景中 的其它信息
//比如
//动态加载怪物
//这时 进度条 再更新20%
//动态加载 场景模型
//这时 就认为 加载结束了 进度条顶满
//隐藏进度条
}
如何通过C#读取XML文件
XmlDocument
首先需要通过这个类实例化一个对象出来→
XmlDocument xml = new XmlDocument();
然后通过API先加载xml文件
有两种API供使用
1. 直接根据xml字符串内容 来加载xml文件
存放在Resorces文件夹下的xml文件加载处理
TextAsset asset = Resources.Load("TestXml");
//Resources资源加载文本文件,类型是TextAsset
xml.LoadXml(asset.text);
2.是通过xml文件的路径去进行加载
xml.Load(Application.streamingAssetsPath + "/TestXml.xml");
//为什么是StreamingAssetsPath?
因为当Unity构建项目时,位于Resources
文件夹内的资源会被编译和打包进游戏包中,并且资源的原始文件路径会丢失,转而由Unity的资源管理系统管理
Unity会将这个文件夹(StreamingAsset)中的内容原封不动地复制到最终游戏包的相应位置。这意味着这些文件在游戏运行时保持不变,可以通过文件系统的路径来访问
读取元素和属性信息
节点信息类
XmlNode 单个节点信息类(不同名的节点获取)
节点列表信息
XmlNodeList 多个节点信息类(一般是有多个同名节点,所以需要一个表来存取多个节点)
无论是获取单个节点还是获取多个节点,都必须先获取跟节点,假设根节点的名字为Root
则获取xml当中的根节点→ XmlNode root = xml.SelectSingleNode("Root");
单个节点信息获取
再通过根节点 去获取下面的子节点
XmlNode nodeName = root.SelectSingleNode("name");
如果想要获取节点包裹的元素信息 直接 .InnerText
print(nodeName.InnerText);
XmlNode nodeAge = root.SelectSingleNode("age"); print(nodeAge.InnerText);
那属性怎么获取呢?
比方说xml文件中有这样一个节点:
XmlNode nodeItem = root.SelectSingleNode("Item");
方式1→
print(nodeItem.Attributes["id"].Value); print(nodeItem.Attributes["num"].Value);
方式2→
print(nodeItem.Attributes.GetNamedItem("id").Value); print(nodeItem.Attributes.GetNamedItem("num").Value);
获取 一个节点下的同名节点的方法:XmlNodeList
比方说在xml文件中有这样两个同名节点:
<Friend>
<name>小明</name>
<age>18</age>
</Friend>
<Friend>
<name>小红</name>
<age>17</age>
</Friend>
XmlNodeList friendList = root.SelectNodes("Friend");
通过遍历的方式去获得每个节点—>
遍历方式一:迭代器遍历
foreach (XmlNode item in friendList)
{
print(item.SelectSingleNode("name").InnerText);
print(item.SelectSingleNode("age").InnerText);
}
遍历方式二:通过for循环遍历
通过XmlNodeList中的 成员变量 Count可以得到 节点数量
for (int i = 0; i < friendList.Count; i++)
{
print(friendList[i].SelectSingleNode("name").InnerText);
print(friendList[i].SelectSingleNode("age").InnerText);
}
总结:
1.读取XML文件
XmlDocument xml = new XmlDocument();
读取文本方式1 - xml.LoadXml(传入xml文本字符串)
读取文本方式2 - xml.Load(传入路径)
2.读取元素和属性
获取单个节点: XmlNode node = xml.SelectSingleNode(节点名)
获取多个节点: XmlNodeList nodeList = xml.SelectNodes(节点名)
获取节点元素内容:node.InnerText
获取节点元素属性:
1.item.Attributes["属性名"].Value
2.item.Attributes.GetNamedItem("属性名").Value
通过迭代器遍历或者循环遍历XmlNodeList对象 可以获取到各单个元素节点