C# in Unity 协程&反射
梚辰 2022.5.3
未经授权禁止转发
1. Coroutines 协程
Coroutines 协程作用
对于任务密集的流程(如A*寻路)将一个任务划分成多个部分,在多个逻辑帧中完成,从而使游戏更流畅。
同时协程可以通过回调来在某任务结束后调用相应函数。
Coroutines 协程使用
-
协程不是多线程,是一种异步多任务处理
*常常容易将协程和多线程混淆,协程是暂停执行此部分的后续代码,等下一帧或满足某些条件时继续执行此部分代码,如果不用
yield return
则会阻滞当前线程;而多线程是异步同时执行,各线程间不会影响 -
使用
IEnumerator
接口,可以认为是返回值为IEnumerator
的函数,或者任意YieldInstruction
类型 -
配合
yield
的特殊语句使用(如yield return ...
) -
由
StartCoroutine()
开启协程,StopCoroutine()
或StopAllCoroutine()
停止协程*
StartCoroutine()
调用时传入方法或方法名,而StopCoroutine()
只传入方法名StartCoroutine(MyCoroutine(args)); StartCoroutine("MyCoroutine",args); StopCoroutine("MyCoroutine"); //MyCoroutine返回值要为IEnumerator
-
常见
yield return
用法yield return null;//下一帧以后执行后续代码 yield return 0;//任意数字,同上 yield break;//直接终止,类似于return yield return StartCoroutine("MyCoroutine");//等待MyCoroutine运行完成时继续执行后面的代码 yield return new WaitForSeconds(time);//等待固定时间,受DeletaTime影响 yield return new WaitForSecondsRealtime(time);//等待固定时间,不受DeletaTime影响 yield return new WaitForEndOfFrame(); //等待直到该帧结束,在Unity渲染每一个摄像机和GUI之后,在屏幕上显示该帧之前。 yield return new WaitUntil(() => time>1);//等到某判断条件为真时执行后续代码 yield return new WaitWhile(() => time>1);//等到某判断条件为假时执行后续代码
同时,我们可以自己定义一个
YieldInstruction
字段,针对不同的条件赋不同的值YieldInstruction time; if(a) time = null; else if(b) time = new WaitForSeconds(1.0f); else time = new WaitForSeconds(2.0f); yield return time;
- 协程在Unity中的应用实例
来自Unity官方《C#中级编程》
using UnityEngine;
using System.Collections;
public class CoroutinesExample : MonoBehaviour
{
public float smoothing = 1f;
public Transform target;
void Start ()
{
StartCoroutine(MyCoroutine(target));//调用协程
}
IEnumerator MyCoroutine (Transform target)
{
while(Vector3.Distance(transform.position, target.position) > 0.05f)
{
transform.position = Vector3.Lerp(transform.position, target.position, smoothing * Time.deltaTime);
yield return null;
}
print("Reached the target.");
yield return new WaitForSeconds(3f);
print("MyCoroutine is now finished.");
}
}
- 与属性结合使用,可以发挥协程的强大作用,通过减少轮询来减少消耗性能
using UnityEngine;
using System.Collections;
public class PropertiesAndCoroutines : MonoBehaviour
{
public float smoothing = 7f;
//Target 通过鼠标点击事件赋值,此处省略
public Vector3 Target
{
get { return target; }
set
{
target = value;
//停止未结束的Movement,开启新的Movement
StopCoroutine("Movement");
StartCoroutine("Movement", target);
}
}
private Vector3 target;
IEnumerator Movement (Vector3 target)
{
while(Vector3.Distance(transform.position, target) > 0.05f)
{
transform.position = Vector3.Lerp(transform.position, target, smoothing * Time.deltaTime);
yield return null;
}
}
}
2. C#反射
从C#内存布局说起
- 类:是一种类型描述,描述这个类型有哪些数据组成,同时描述一些成员函数
- 类的实例:
new()
一个类的实例,是一个具体的内存对象,指向一块内存,这块内存是所有数据成员的集合- 类的成员函数属于代码指令,所有类的实例共用一份代码指令
this
实例:成员函数里面使用this
,则指的是当前对象实例,通过this
来操作当前对象实例的这块内存- 编好一个类型后,编译器知道每个数据相对于对象实例内存块的偏移,也知道每个类的成员函数在代码段的偏移
C#反射及其作用
以Unity引擎为例:Unity是怎么加载脚本,读取脚本里的组件类型,并给物体添加该类型组件?
运用C#反射
由于每一个类都有独立的描述,所以我们新加一个类,就会多一种方式来描述,所有没有办法使用统一的方式来处理不同类或类的实例,所以需要用一种方式来描述所有类。
新的描述方式:
- 类的实例是一个内存块,内存块的大小就是这个类所有数据成员的大小 ----> 类实例的内存块大小
- 类有哪些数据成员,可以把数据成员通过数组等方式保存:
{名字,类型,偏移量} - 类有哪些成员函数,通过数组等方式保存:
{成员函数名字,类型,代码段位置}
通过这种方式,任意的类都可以转化成一种描述,从而解决上面的问题。
每个类,编译器知道其数据偏移,函数代码段位置等信息,可以在运行时为其申请一块内存空间,并通过描述信息来创建实例,并为这个类创建一个type。
通过这种方法,编译器可以为任何类创建实例,然后将内存块传递给构造函数,构造函数帮助初始化数据。
使用方法
Type t = System.Type.GetType(name);
gameObject.AddComponent(t);
- Type里面存放了类的描述信息,根据这种描述信息并结合实例就可以把数据信息取出来
- 针对数据信息,运用
SetValue()
/GetValue()
对数据进行访问或者修改 - 针对成员函数信息,运用
getMethod(函数名)
/methodInfo.Invoke(实例,参数列表)
来获取函数信息或调用函数,函数返回值为Object
类
- 针对数据信息,运用
C#反射具体使用
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Reflection;
public class Test
{
public int data1;
public float data2;
//...
public int test(int param1, float param2, string param3)
{
//...
return 1;
}
}
public class ReflectTutorial:MonoBehaviour
{
void Start()
{
//获取 类型描述Type 的实例
Type t = System.Type.GetType("Test");
//若非unity中则需要注明名称空间("名称空间名.类名")
//根据此 类型描述Type 的实例,构建出一个对象
var instance = Activator.CreateInstance(t);
//利用我们存放的数据成员信息给它们赋值
// 根据 instance + (偏移 + 内容) 来访问或修改数据
FieldInfo[] fieldsinfo = t.GetFields();//获取所有数据
FieldInfo data1Info = t.GetField("data1");//获取单个数据
data1Info.SetValue(instance, 新的属性值);//设置属性值
Debug.Log((instance as Test).data1);
//调用成员函数
//获取函数信息
MethodInfo m = t.GetMethod("test");
System.Object[] funcParams = {1, 1.0f, "111"};
int? result = m.Invoke(instance, funcParams) as int?;
//Invoke返回值可能为空
//注:as必须与引用类型或者可以为null的类型一块使用
Debug.Log(result);
}
}