写在前面
写本系列的目的(自用)是回顾已经学过的知识、记录新学习的知识或是记录心得理解,方便自己以后快速复习,减少遗忘。这里只有部分C#相关语法知识,内容是跟着唐老师学的。
一、生命周期函数
1、帧的概念
游戏本质与帧的关系:游戏的本质就是⼀个死循环,每次循环处理游戏逻辑就会更新⼀次画面、当切换画面的速度达到⼀定时(通常24帧/秒),⼈眼就会认为画面是流畅的。
常见帧率:
60帧:每帧时间=1000ms / 60 ≈ 16.66ms
30帧:每帧时间=1000ms / 30 ≈ 33.33ms
⼈眼舒适帧率:人眼在放松状态下可视帧数为每秒24帧
Unity已经内置了死循环机制,开发者不需要自己编写while循环,通过使用Unity提供的生命周期函数来处理游戏逻辑。
2、生命周期函数
(1)Awake函数
Awake是Unity中的生命周期函数,在对象创建时自动调用。类似构造函数的存在(但不能带参数),用于对象创建时的初始化操作。⼀个对象在其生命周期中只会调用⼀次(仅与脚本实例创建有关)。
不写访问修饰符时默认是私有函数。
(2)OnEnable生命周期函数
当脚本依附的GameObject对象每次激活时自动调用。且相同来源的打印信息会被折叠显示,但不同激活方式(初始创建vs手动激活)会产生不同来源的打印
用处是,当需要对象被激活时执行特定逻辑处理的场景,可以编写OnEnable生命周期函数。
(3)Start生命周期函数
从对象被创建出来后,第⼀次帧更新之前调用。⼀个对象只会调用一次。
与Awake的区别是:
Awake:对象生成时立即调用,类似构造函数
Start:在Awake之后,第⼀次帧更新之前调用
例如,在Update中动态创建的对象会立即执行Awake,Start会等到当前帧结束后,下⼀帧开始前才执行。
用处是用于对象初始化。
(4)FixedUpdate生命周期函数
主要用于进行物理相关的处理,如碰撞检测等物理计算。如果不做物理相关的游戏开发,基本上很少使用该函数。
Unity内部实现了⼀个死循环,以固定间隔时间(物理帧间隔)执行FixedUpdate
(5)Update生命周期函数
处理游戏核心逻辑更新,如位移计算等;每帧执行,帧率可通过项目设置控制;Unity通过反射在每次游戏循环时自动调用;是编写游戏逻辑的主要接口,会频繁使用。
例如,在Update中,可以通过每帧修改位置实现平滑移动效果。
(6)LateUpdate生命周期函数
处理摄像机位置更新等渲染相关逻辑,在Update之后执行,两者之间Unity会处理动画更新,这样可以避免在Update中更新摄像机导致的渲染错误。
(7)OnDisable生命周期函数
当依附的GameObject对象每次失活时调⽤,与OnEnable形成对应关系。
对象失活后,所有生命周期函数(特别是循环更新类函数)都会停止调用。
核心作用:用于在对象失活时进行特定逻辑处理,适合存放状态保存、资源释放等代码
(8)OnDestroy生命周期函数
当依附的GameObject对象被销毁(从场景中移除)时调用。
销毁时会先调用OnDisable再调⽤OnDestroy,形成完整生命周期闭环。只会调用一次。
二、Inspector窗口的变量
1、让私有的和保护的也可以被显示和编辑
在脚本中创建的变量,如果不是public的,在Inspector面板上就不会被显示。
但也可以通过特性,让私有的和保护的也可以被显示和编辑。
可以在变量前添加[SerializeField]特性,这样私有的和保护的变量都可以在面板上显示和编辑。
public class inspec : MonoBehaviour
{
[SerializeField]
private int a = 10;
}
面板:
2、让公共的不能被显示和编辑
在脚本中创建的变量,可以通过特性,让公共的也不可以被显示和编辑。
可以在变量前添加[HideInInspector]特性,这样公共的也不可以在面板上显示和编辑。
public class inspec : MonoBehaviour
{
[HideInInspector]
public string str = "HELLO";
}
3、小部分类型有显示限制
我们所熟知的各种类:数组、列表、枚举、整型、浮点型、字符串等,只要是公共的或是加了可编辑特性的,都可以在Inspector面板上显示和编辑。
但字典、自定义结构体、自定义类哪怕是公共的也不支持显示。Unity默认不支持复杂数据结构的序列化显示。
但如果在自定义结构体或是自定义类前,加上特性[System.Serializable],那么自定义的结构体或是自定义类也能够显示并编辑了
[System.Serializable]
public class Myclass
{
public string name;
public int age;
}
public class inspec : MonoBehaviour
{
public Myclass myclass;
}
但是字典仍然不可以。
三、MonoBehavior
1、重要成员
(1)获取依附的GameObject
通过MonoBehavior中的成员,可以获取当前脚本依附的对象的各种信息。名字、位置、将当前依附的脚本进行激活、失活。
以下是获取信息的函数。
void Start()
{
//获取名字
print(this.gameObject.name);
//获取位置信息
//获取位置
print(this.transform.position);
//获取角度
print(this.transform.eulerAngles);
//获取缩放大小
print(this.transform.lossyScale);
}
将当前脚本激活/失活可以用this.enabled = true/false:
void Start()
{
//激活
this.enabled = true;
//失活
this.enabled = false;
}
2、重要方法
(1)获取挂载的脚本
可以利用函数GetComponent的泛型方法来获取当前对象挂载的脚本。
void Start()
{
//根据泛型获取
mono t = this.GetComponent<mono>();
print(t);
}
(2)获取挂载的多个脚本
假如对象上挂载了多个相同的脚本,可以用this.GetComponents来得到挂载的所有脚本。
void Start()
{
mono[] array = this.GetComponents<mono>();
print(array.Length);
}
(3)获取子对象挂载的脚本
可以使用this.GetComponentInChildren<T>()获取挂载到子对象上的脚本(它默认也会找自己身上是否挂载该脚本)
括号内可以传参数,默认不传是false。假如穿了true,那么哪怕是失活的脚本也会去找,false默认失活的不找
同样的,也可以获取子对象挂载的多个脚本,添个s即可:this.GetComponentsInChildren<T>()
void Start()
{
mono2 t2 = this.GetComponentInChildren<mono2>(true);
print(t2);
mono2[] array2 = this.GetComponentsInChildren<mono2>(true);
print(array.Length);
}
(4)获取父对象挂载的脚本
可以使用this.GetComponentInParent<T>()获取挂载到父对象上的脚本(它默认也会找自己身上是否挂载该脚本)
需要注意的是,找父对象的脚本时一般不会传入是否找失活对象。因为父对象失活时,子对象也会失活,没有意义。
同样的,也可以获取父对象挂载的多个脚本,添s即可:this.GetComponentsInParent<mono2>()
void Start()
{
mono_f t3 = this.GetComponentInParent<mono_f>();
print(t3);
mono2[] array3 = this.GetComponentsInParent<mono2>();
print(array3.Length);
}
四、最小单位GameObject
GameObject是Unity场景中的最小单位,所有脚本都依附在GameObject上运行。
1、GameObject成员变量
(1)获取名字
可以通过this.gameObject.name获取当前对象名称。也可以直接对该值赋值事实修改场景中的对象名称。
void Start()
{
print(this.gameObject.name);
this.gameObject.name = "MyCube";
print(this.gameObject.name);
}
执行结果:
(2)是否激活、是否是静态
判断对象是否激活可以用.activeSelf,判断是否是静态可以用.isStatic
void Start()
{
print(this.gameObject.activeSelf);
print(this.gameObject.isStatic);
}
(3)获取层级和标签
获取层级:gameObject.layer,层级默认为0;获取标签:gameObject.tag,未设置标签时返回"Untagged"
void Start()
{
print(this.gameObject.layer);
print(this.gameObject.tag);
}
(4)Transform
可以通过this.gameObject.transform.position获取详细的三维坐标,也可以直接通过this.transform.position获取。推荐后者:
print(this.transform.position);
结果Unity会自行四舍五入。
2、GameObject静态方法
(1)创建自带几何体
可以通过GameObject.CreatePrimitive(PrimitiveType)静态方法创建Unity内置几何体。如下例,创建一个自带的Cube立方体并为其改名。
void Start()
{
GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
obj.name = "创建的立方体";
}
运行结果:
(2)查找对象
①可以通过按名查找和按tag查找。
按名查找:使用GameObject.Find("对象名")静态方法。缺点是效率较低,因为会遍历场景中所有对象,假如没找到,返回null
按tag查找: 有两种方法,效果一样,GameObject.FindWithTag("标签名")、GameObject.FindGameObjectWithTag("标签名")。
需要注意的是,二者都无法查找到失活的对象。 假如场景中有多个同名或是同tag对象时,查找结果是随机的,无法指定找到哪个对象。
void Start()
{
GameObject obj2 = GameObject.Find("创建的立方体");
if(obj2 != null)
{
print(obj2.name);
}
GameObject obj3 = GameObject.FindWithTag("Player");
if(obj3 != null)
{
print(obj3.name);
}
GameObject obj4 = GameObject.FindGameObjectWithTag("Player");
}
②查找多个对象
查找多个对象只能用GameObject.FindGameObjectWithTag("tag名"),且同样只能找到激活对象
void Start()
{
GameObject[] objs = GameObject.FindGameObjectsWithTag("Player");
print("查找到的数量" + objs.Length);
}
③通过泛型查找对象
可以通过FindObjectOfType<T>():查找挂载特定脚本的对象,返回的是脚本组件实例。
void Start()
{
GB_1 o = GameObject.FindObjectOfType<GB_1>();
print(o.gameObject.name);
}
(3)实例化(克隆)对象
实例化(克隆)对象是根据⼀个GameObject对象创建和它完全相同的对象。克隆的对象包含原对象的所有脚本信息和组件。
使用GameObject.Instantiate()方法实现克隆功能。克隆的对象可以选择场景上的对象,也可以是预设体对象。
public GameObject obj;
void Start()
{
GameObject.Instantiate(obj);
}
关联预设体:
运行结果:
(4)删除对象
GameObject.Destroy()方法可以删除游戏对象或脚本组件。可以传1~2个参数,第一个参数是需要删除的对象,第二个参数是延迟多少秒删除。
public GameObject obj;
public GameObject obj2;
// Start is called before the first frame update
void Start()
{
GameObject.Instantiate(obj);
GameObject.Destroy(obj2);
//延迟5s删除
GameObject.Destroy(obj, 5);
//删除当前脚本组件,Inspector窗口中该脚本组件会消失
GameObject.Destroy(this);
}
GameObject.Destroy()方法实际上不会立即移除对象,只是添加了移除标识。通常会在下一帧时真正移除对象。如果需要立即删除,需要使用:DestroyImmediate()
(5)过场景不移除
默认情况下,在切换场景时,所有场景对象会被自动删除。如果不希望某个对象被删除,就可以使用:
GameObject.DontDestroyOnLoad(this.gameObject);
这样,该对象过场景就不会被移除。
3、GameObject成员方法
(1)创建空物体
创建空物体直接使用new即可,就可以在场景里创建一个空物体。可以传入参数,为空物体命名、挂载脚本。
添加脚本使用泛型方法 obj.AddComponent<T>即可,添加的脚本有返回值,返回值是添加的脚本,可以用变量接收。
void Start()
{
GameObject obj = new GameObject();
GameObject obj2 = new GameObject("空物体");
GameObject obj3 = new GameObject("加脚本的空物体",typeof(temp));
temp tp = obj2.AddComponent<temp>();
}
查看加脚本的Inspector信息:
(2)标签比较
标签比较有两种方法,第一种是使用gameObject.CompareTag(),会比较当前对象的tag名和括号中传入的tag名
第二种是直接判断gameObject.tag == “tag名”,两种方法完全一样。
void Start()
{
if(this.gameObject.CompareTag("Player"))
{
print("对象的标签是 Player");
}
if(this.gameObject.tag == "Player")
{
print("对象的标签是 Player");
}
}
(3)设置对象激活失活
设置对象激活失活可以使用obj.SetActive(),括号里填false时,对象就会失活。
void Start()
{
GameObject obj = new GameObject();
GameObject obj2 = new GameObject("空物体");
GameObject obj3 = new GameObject("加脚本的空物体",typeof(temp));
obj.SetActive(false);
obj2.SetActive(false);
obj3.SetActive(false);
}
(4)广播或发送消息
了解即可,不建议使用。
SendMessage()主要是通知自己要执行什么行为。例如下例,通知自己执行函数TestFun。于是会在自己身上挂载的所有脚本中去找这个名字的函数,效率很低。
void Start()
{
this.gameObject.SendMessage("TestFun");
}
void TestFun()
{
print("执行TestFun");
}
gameObject. BroadcastMessage("函数名")是广播行为,会通知自己和自己的子对象去执行这个函数。
this.gameObject.BroadcastMessage("TestFun");
gameObject.SendMessageUpwards("函数名")也是广播行为,会通知自己和自己的父对象去执行这个函数。
this.gameObject.SendMessageUpwards("TestFun");