FormerlySerializedAs
如果之前代码中的一个变量名字看他很不爽,想要给他改一个名字,改完之后回到 Unity 就会发现在 Inspector 面板上面那个引用已经为空了,这时只需要使用这一条语句就可以保留原来名字的引用。具体实验如下:
Step1:新建一个脚本,这里使用的是 Private 成员,也可以使用 Public 的,Public 默认会被序列化,就不需要加 [SerializeField] 了。
public class NewBehaviourScript : MonoBehaviour
{
[SerializeField]
private string MyString;
}
这个时候 Inspector 面板显示如下:
Step2:这个时候你想将 MyString 这个变量名改成 MyBatterStringName,那么就改吧~
public class NewBehaviourScript : MonoBehaviour
{
[SerializeField]
private string MyBatterStringName;
}
此时 Inspector 面板变成了:
是的,它的引用不见了,如果这里的 string 是 GameObject 的话就会变成 Null,运行就有可能会报空指针异常。
Step3:我们再修改一下代码:
public class NewBehaviourScript : MonoBehaviour
{
[FormerlySerializedAs("MyString")]
[SerializeField]
private string MyBatterStringName;
}
然后保存一下回到 Unity,会发现依然没有变化,那我们再重新恢复到 Step1 完成之后的样子,直接跳过 Step2,到 Step3,保存一下发现我们的 String 的内容没有被重置。
总结:所以对于序列化的成员,我们以后在改名字的时候可以顺手加上
[FormerlySerializedAs("OldName")]
,这样就不用在 Inspector面板重新赋值了,多次改名保留上一次的名字就好了,也就是只需要保留一条这个语句。
Mathf.PingPang
这是 UGUI 的 Graphic 类中的一个小栗子。效果感觉跟调节 Image 的 Inspector 面板上面的 Color 一样,运行结果如下:
代码如下:
using UnityEngine;
using UnityEngine.UI;
public class Test : MonoBehaviour
{
Graphic m_Graphic;
Color m_MyColor;
void Start()
{
//Fetch the Graphic from the GameObject
m_Graphic = GetComponent<Graphic>();
//Create a new Color that starts as red
m_MyColor = Color.red;
//Change the Graphic Color to the new Color
m_Graphic.color = m_MyColor;
}
// Update is called once per frame
void Update()
{
Debug.Log(Mathf.PingPong(Time.time, 1));
//Debug.Log(Time.time);
//When the mouse button is clicked, change the Graphic Color
if (Input.GetKey(KeyCode.Mouse0))
{
//Change the Color over time between blue and red while the mouse button is pressed
m_MyColor = Color.Lerp(Color.red, Color.black, Mathf.PingPong(Time.time, 1));
}
//Change the Graphic Color to the new Color
m_Graphic.color = m_MyColor;
}
}
Lerp 函数的 t 范围为 0-1 的一个值,0 的时候为结果为 颜色 a,也就是这里的 Color.red,t=1 的时候结果为颜色 b,也就是这里的 Color.black 的颜色(Vector3.Lerp 和 Mathf.Lerp 也只这样子哦~ 如果 t 为 0.5 的话,那么结果就是 a 和 b 的中间颜色/中点/中间值),而 PingPang 的第一个参数需要一个自增量,一般设置为 Time.time
,然后第一个参数是 length,会不断返回 0-length 之间的一个值,具体可以看上面运行结果下方的控制台输出值,就是当前的 PingPang 返回值。(所以 PingPang 和 Lerp 真是绝配)
TODO: 在这个例子中不知道如何控制变换速度,应该要从 PingPang 的返回值入手,可能需要自己定义一个自增值。
internal
internal 限制了只能在同一个程序集中进行访问,下面是 Unity 中的脚本编译规则
脚本编辑规则:
最终所有代码都会生成 dll,放在 Project/Library/ScriptAssembiles 下面。
脚本分为运行时和编辑时两类,运行时脚本最终会编译进游戏包中,而编辑时脚本仅用于编辑器模式下,不会被打包进游戏包。
脚本编译顺序:
最先编译 Plugins 目录下的,然后是 Plugins 下的所有 Editor 子目录,然后编译其他目录,最后编译其他 Editor 目录。
先编译的不可以访问后面的数据,所以 Plugins 下的代码不能访问其他代码,后编译的可以访问先编译的脚本。
各目录脚本最终所在的 dll:
Plugins 下非 Editor 目录脚本编译进 Assembly-CSharp-firstpass.dll
Plugins 下的 Editor 目录脚本编译进 Assembly-CSharp-Editor-firstpass.dll
其他非 Editor 目录脚本编译进 Assembly-CSharp.dll
其他 Editor 目录脚本编译进 Assembly-CSharp-Editor.dll
所以可以看到在 Editor 下面的代码和非 Editor 下面的代码是在不同的程序集的,测试一下在非 Editor 目录下写一个类:
namespace ZhangqrTools
{
internal class ObjectPool<T> where T : new()
{
}
}
然后在 Editor 目录下写一个测试类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using ZhangqrTools;
public class TestEditor : MonoBehaviour
{
void Start()
{
ObjectPool<List<int>> obp = new ZhangqrTools.ObjectPool<List<int>>();
}
}
会发现报错了,说 不可访问,因为有一定的保护级别
。将 internal
改为 Public
之后就可以访问了。
Texture Sheet Animation
官方 Texture Sheet Animation
制作了一张这样的图(接缝处可能会有几像素的偏差),然后导到 Unity 之后图片设置就默认,2D就好了。
做一个标准 Shader 的材质球,渲染模式因为这张图没有透明的部分,所以选啥都可以。
因为这个图有 X 轴上被分为 4 块,Y 轴被分为 5 块,所以按照下面的设置
效果为:
可以看到在整个例子的生命周期,是按照这样的顺序逐帧播放的动画:
现在将设置改为:
运行结果为:
可以看到它只会播放第一行的动画了。随机行数之后,每个例子随机选一行在整个生命周期完成动画。(换了个背景,不然看不清楚)
上面都是随着例子生命的流逝而做的帧动画,下面是 FPS,FPS 越大,变换越快。
Speed 感觉要复杂一点:
先给例子加一个 X 轴的加速度:(这个线性速度我觉得应该是相加,因为现在我的粒子初始速度是设置为 5 的,后面经过测试,我觉得它整个生命周期的速度应该在 [5,20] 的样子,也就是 5 加上这个曲线。
可以看到4个颜色分布还是较均匀的,然后我们把速度范围设置为 [5,65],也就是让 [5,20] 为前 1/4。结果如下:
可以看到基本只有一种颜色,也就是前 1/4 的颜色。再把范围速度范围设置为 [5,35],也就是让 [5,20] 为前 1/2。结果如下:
可以看到基本是有两种颜色,也就是前 1/2 的颜色,并且分布较均匀。再把速度范围设置为 [5,25],也就是让 [5,20] 为前 3/4。结果如下:
现在就有三种颜色了。所以最后总结一下。假设我们的例子整个生命周期的速度是从 [5,20] 线性变换,我们的速度范围为 [5,35],那么整个色带(也就是动画的逐帧图)会均匀的分布在 [5,35] 的范围,真实的速度 [5,20] 会截取前 1/2。前后会用第一张图和最后一张图补齐。
最后提一下,粒子效果中怎么保留图片的不透明度,起因是想要做一个麦粒倾泻的效果。参考火焰的效果,可以做一张背景是黑色的图片,然后按下面进行设置:
可以看到效果比较梦幻,如果素材是火焰的话就会比较好看,但我需要的是现实中的麦粒,不是天堂的麦粒。所以做了一张背景是透明的图片,改用标准的 Shader,渲染模式设置为 Cutout
最终效果
获取模型最低点
比如要做一个倒水的效果,水面是一个贴图,需要计算出水点,就可以用这种方法。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
private Vector3[] vertices;
Vector3 v_min = Vector3.zero;
private void Start()
{
Vector3 size = this.GetComponent<MeshFilter>().mesh.bounds.size;
Debug.Log(size.x * transform.localScale.x); // 1 因为 Plane 模型大小是 10,Scale 是 0.1。bounds.size 通常等于两倍的 bounds.extents
}
private void Update()
{
vertices = this.GetComponent<MeshFilter>().mesh.vertices; // 与模型的相对坐标
for (int i = 0; i < vertices.Length; ++i)
{
Vector3 world_position = transform.TransformPoint(vertices[i]); // 转换成世界坐标
if (i == 0)
{
v_min = world_position;
}
else
{
if (world_position.y < v_min.y)
{
v_min = world_position;
}
}
}
Debug.Log("y 的最小值为:" + v_min.y);
}
}
球形差值
public static Vector3 Slerp(Vector3 a, Vector3 b, float t);
这个函数,可以理解为 a 为第一个方向,b 为第二个方向,然后 t 为 0.5 的话,就会在 ∠AB 的角平分画上有一个点,长度是根据 a b 的长度来的,也就是说 t 从 0-1 的过程,所有点组成的线会平滑的穿过 a b 的端点。
烟花
下面是使用 visual effect graph
制作的烟花。
教程:https://www.bilibili.com/video/BV14E411c76W
FixedUpdate 和 Input
在一个移动的物体的 FixedUpdate 中这么写:
if (Input.GetKey(KeyCode.H))
{
//////////////////////////////
GameObject ob = GameObject.CreatePrimitive(PrimitiveType.Sphere);
ob.transform.position = transform.position;
ob.transform.localScale = Vector3.one * 0.02f;
}
和去掉 Input 的判断,最终生成的小球数量是不一样的,不加 Input 判断生成的小球明显要更多一点。 所以即使 FixedUpdate 的执行次数要更多,但在 Input 里面的内容还是根据 Update 的帧数来(可能是这样的吧,因为 Input 一般都写在 Update 里面)。
父子
foreach(Transform t in transform) // 只能找一层子物体,不管子物体是否处于激活状态
foreach(Transform t in transform.GetComponentsInChildren<Transform>()) // 只会找激活的,包括子子孙孙,也包括父物体
foreach(Transform t in transform.GetComponentsInChildren<Transform>(true)) // 激活的不激活的都可以,包括子子孙孙,也包括父物体
GameObject.Find("Test") // 只寻找场景中激活的物体,不管在第多少层
public void SetParent(Transform parent, bool worldPositionStays);
第二个参数为 True:物体世界位置,旋转,缩放不会发生改变
第二个参数为 False:物体的局部位置,旋转,缩放不会发生改变
Text 首行缩进 And 换行
在前面加上这个 <color=#FFFFFF00>------</color>
可以实现首行缩进,不论是在编辑器面板中的字符还是代码中的。
\n
一个比较坑的点是,在编辑其中的 Text 中写 \n 没用,因为 Unity 会自动把 \n
换成 \\n
,所以需要我们在代码中给他变回来 text.Replace("\\n", "\n")
;在代码中用 \n
没有问题。
Resources.Load
- 比如一个预制体,在编辑器中直接 Copy Path 的话,他会把后缀名也带上,但是带上的话就找不到了,一定不能带后缀名
-
Resources.Load
只是加载进内存,需要实例化才能在 Scene 中使用,如果没有实例化直接使用,那么改变的是预制体,而不是 Scene 中的实例。
if(HandId == 0)
{
hand = Resources.Load<GameObject>(ConstName.resources_hand_left);
}
else
{
hand = Resources.Load<GameObject>(ConstName.resources_hand_right);
}
hand = Instantiate(hand);
h = hand.GetComponent<Hand>();
协程
- 协程执行结束之后是不会变成 null 的,所以不能用 null 来判断协程是否结束。
- StopCoroutine();内不能传 null,所以在 Stop 的时候要小心此时协程是否被赋值了。而且 Stop 之后,这个协程的值依然不是 null。
- 那么怎么判断一个协程是否结束呢?目前采用的方式是使用一个成员变量。
OnTriggerEnter
如果把当前的脚本禁用了(也就是 enable 设为了 false),OnTriggerEnter 依然会被执行。