发布时间:2023-06-05 13:30
接着前面脚本优化讲,我们还是深入的讲解脚本该如何优化。在其它的一些情景中,我们希望组件或对象被禁用后,他们能够离玩家足够远,这样他们可能是勉强可见的,但是又离对象特别远。一个贴切的例子是AI生物的巡逻,我们可以在一定的距离内看见,但是我们不需要它们处理任何事情。下面的代码是一个简单的程序,用来定期检测到目标的总距离并且如果靠的太远就禁用自身。
[SerializeField] GameObject _target;
[SerializeField] float _maxDistance;
[SerializeField] int _coroutineFrequency;
void Start() {
StartCoroutine(DisableAtADistance());
}
IEnumerator DisableAtADistance() {
while(true) {
float distSqrd = (Transform.position -
_target.transform.position).sqrMagnitude;
if (distSqrd < _maxDistance * _maxDistance)
{
enabled = true;
}
else
{
enabled = false;
}
for (int i = 0; i < _coroutineFrequency; ++i) {
yield return new WaitForEndOfFrame();
}
}
}
我们应该在Inspector面版分配玩家对象(或任何我们想比较对象)给_target 字段,在_maxdistance定义最大距离,通过_coroutineFrequency属性在每次协程被调用时修改频率。任何时候对象和指定对象间的距离超出最大距离,它将变得不可见。在最大距离内就重新启用。一个小的优化是用距离的平方而不是用距离本身,这会方便我们的下一个提示。可以说,CPU比较擅长浮点数乘法,但是计算平方根就不是很好了。每次我们调用Vector3的magnitude属性去计算距离或者用Distance()方法的时候,我们要求它进行一个平方根运算(按照毕达哥拉斯定理)。与许多其他类型的矢量数学计算相比,它可以花费大量的处理器开销。
不过,Vector3还提供了一个sqrMagnitude 属性,是用来计算距离的平方的。这让我们执行了基本相同的比较而不用花费高昂的平方根代价,而如果我们直接写距离的平方会显得太过冗长;或者可以这么描述这个数学问题,A的模长小于B的模长,那么A的平方同样小于B的平方。
举个例子,代码如下:
float distance = (transform.position –other.transform.position).Distance();
if (distance < targetDistance) {
// do stuff
}
这可以被下面的替代得到相同的结果:
float distanceSqrd = (transform.position –other.transform.position).sqrMagnitude;
if (distanceSqrd < targetDistance * targetDistance) {
// do stuff
}
因为浮点精度(影响不大)其结果几乎是相同的。我们很可能会失去一些我们本来可以使用的平方根值的精度,因为该值被调到一个不同精度表示的数的范围;它可能更准确更接近的表示该数,也可能减少一定的精度。因此,比较的结果并不完全相同,但是在大多数情况下,它是足够接近而不会出错的,并且在这种情况下获得的效率提升是足够大的。如果这个小的精度损失对你不重要,这个性能的技巧是应该被考虑的。然而,如果精度是非常重要的(如运行一个准确的大型星系空间模拟),你就应该寻找其他地方的性能改进。请注意,该技术可用于任何平方根的计算,而不仅仅是距离。这是你可能遇到的最常见的例子,来明确Vector3中sqrMagnitude 属性的重要性-------Unity技术故意暴露给我们的一个属性。前面讲到了一点分发的原理,这里我还是强调一下吧。优化更新的另一种方法是不使用update(),更准确的说是只使用一次。当Unity调用 Update() ,它涉及到桥接游戏对象本身和管理对象的表现,这可能是一个代价高昂的任务。因此,我们可以通过限制它的桥接最大程度的限制它的开销。在我们所有的自定义组件中,我们可以用一个最高的管理类来调用我们自己定义的执行Update功能的函数。事实上,许多Unity的开发人员更喜欢这种方法作为他们项目的开始,以便更精细准确的对整个系统进行控
制管理,比如菜单暂停和冷却时间操作的效果。所有想要集成这样一个系统的对象必须有一个共同的入口点。我们可以通过一个接口类实现这个。接口基本上建立了一个契约,任何继承该接口的类必须实现一系列的方法。换句话说,如果我们知道对象实现了接口,然后我们可以确定什么方法是可用的。在C#中,每个类只能有一个基类,但是它们可以继承多接口(这避免了C++程序员可能会遇到的“deadly diamond of death”问题)。
下面的接口定义就足够了,只需要派生类实现一个单一的方法:
public interface IUpdateable {
void OnUpdate(float dt);
}
接着,我们要定义MonoBehaviour类,让它继承该接口:
public class UpdateableMonoBehaviour : MonoBehaviour, IUpdateable
{
public virtual void OnUpdate(float dt) {}
}
UpdateableMonoBehaviour 的OnUpdate()方法需要检索当前的时间增量dt,节省我们一些不必要的Time.deltaTime的调用。我们还声明了虚方法,允许派生类来定义它。然而,正如你所知道的,Unity会自动调用名字为Update()的方法,但是我们自定义的update 用了一个不同的名字,我们需要在合适的时间对这些方法进行调用,比如在一些GameLogic的管理类。在这个组件的初始化过程中,我们应该做些什么来通知我们的游戏逻辑的它存在和它的销毁,以便于知道何时启用和停止该onupdate()方法。在下面的例子,我们假设我们的游戏逻辑类是单例组件,正如在题为“单例”组件的章节中定义的那样,定义适当的静态函数进行注册和注销(虽然它可以很容易地使用我们的信息系统!)。将MonoBehaviours与这个系统挂钩,最合适的地方是在start()和ondestroy():
void Start() {
GameLogic.Instance.RegisterUpdateableObject(this);
}
void OnDestroy() {
GameLogic.Instance.DeregisterUpdateableObject(this);
}
最好使用start()方法完成这个任务,使用Start()意味着我们可以确定所有需要提前存在的对象已经在这一刻在Awake()方法的调用中存在了。通过这种方式,任何关键的初始化工作都将在我们开始调用它的更新之前就已经完成了。注意,因为我们在MonoBehaviour基类中使用Start()方法,如果我们在派生类定义Start()方法,它将有效的重写了基类的定义,并且Unity会自动调用派生类的start()方法。聪明的办法是实现一个Initialize() 的虚方法,派生类可以重写它定制的初始化行为,而不会干扰我们组件的GameLogic 对象基类的任务通知。
void Start() {
GameLogic.Instance.RegisterUpdateableObject(this);
Initialize();
}
protected virtual void Initialize() {
// derived classes should override this method for initialization code
}
我们应该尽可能的让这个过程自动完成,为了让我们不必再为我们定义的每一个新的组件重新执行这些任务。当一个类继承自updateablemonobehaviour类,在合适的时候调用OnUpdate()方法就将是安全的。最后,我们需要实现游戏逻辑类。无论是单例组件还是独立组件,该实现都是相同的,也不管它是不是使用信息系统。无论哪种方式,作
为iupdateableobject对象我们updateablemonobehaviour类必须注册和注销,游戏逻辑类必须使用自己的update()回调,来迭代每个注册的对象调用OnUpdate()函数。
public class GameLogic : SingletonAsComponent
{
public static GameLogic Instance
{
get { return ((GameLogic)_Instance); }
set { _Instance = value; }
}
List
public void RegisterUpdateableObject(IUpdateableObject obj)
{
if (!_Instance._updateableObjects.Contains(obj)) {
_Instance._updateableObjects.Add(obj);
}
public void DeregisterUpdateableObject(IUpdateableObject obj) {
if (_Instance._updateableObjects.Contains(obj)) {
_Instance._updateableObjects.Remove(obj);
}
}
void Update() {
float dt = Time.deltaTime;
for(int i = 0; i < _Instance._updateableObjects.Count; ++i) {
_Instance._updateableObjects[i].OnUpdate(dt);
}
}
}
如果我们确保所有我们自定义的MonoBehaviours 类继承自UpdateableMonoBehaviour 类,我们就有效的用了一个 Update()的调用替代了N个Update()的调用,以及N个虚方法的调用。这可以节省大量的性能开销,因为即使我们调用虚拟函数,我们仍然需要保持绝大多数的更新行为在托管代码,尽可能避免本地托管桥。取决于你进行你项目的深度,这样的变化是非常艰巨的,耗时的,当子系统更新的时候会引入大量的bug,从而使用了一个完全不同的依赖集。但是如果你有时间,它的好处也要超过你的风险。在场景中用用一组对象进行测试是比较明智的,这样可以通过场景文件验证你的
好处是否超过你的风险。
动漫人物也能变“真人”?PaddleGAN帮你找到“撕漫”的TA
【MQTT从入门到提高系列 | 01】从0到1快速搭建MQTT测试环境
R语言惩罚逻辑回归、线性判别分析LDA、广义加性模型GAM、多元自适应回归样条MARS、KNN、二次判别分析QDA、决策树、随机森林、支持向量机SVM分类优质劣质葡萄酒十折交叉验证和ROC可视化
java Spring Cloud+Spring boot+mybatis 工程管理系统源码之SpringCloud Apollo本地部署详细步骤
【图像增强】基于Frangi滤波器实现血管图像增强附matlab代码
STM32启动文件详解——startup_stm32f10x_xx.s
Comparable 和 Comparator 之 深度解析与对比
Cisco三层交换机实现vlan间路由讲解与配置命令,配置过程很详细
【C语言进阶篇】字符函数和字符串函数——strstr&&strtok&&strerror&&strncpy&&strncat&&strcmp函数