本文简述了一种基于时间片(Timeslice)的对象更新(Update)方法
游戏开发中,我们一般都要进行大量的对象更新(Update)操作,拿 Unity 中的 MonoBehavior 举例,其便支持定义专门的 Update 方法来处理相关 Component 的更新:
using UnityEngine;
public class CustomScript : MonoBehaviour
{
void Update()
{
// do CustomScript update here ...
}
}
但是,当更新操作需要事件触发时(MonoBehaviour 的 Update 方法每帧都会执行,属于轮询触发),上述的方法就不太合适了,更好的一种方式是游戏逻辑自己来管理相关对象的更新触发:
using UnityEngine;
public class UpdateObject
{
public void Update()
{
// do UpdateObject update here ...
}
}
using System.Collections.Generic;
using UnityEngine;
public class UpdateManager : MonoBehaviour
{
List<UpdateObject> m_objectList = new List<UpdateObject>();
public void UpdateRequest(UpdateObject uo)
{
Debug.Assert(uo != null);
if (!m_objectList.Contains(uo))
{
m_objectList.Add(uo);
}
}
void Update()
{
if (m_objectList.Count > 0)
{
for (int i = 0; i < m_objectList.Count; ++i)
{
m_objectList[i].Update();
}
m_objectList.Clear();
}
}
}
上述代码中的 UpdateManager 便用于管理相关对象的更新触发操作(通过 UpdateRequest 方法).
现在我们解决了事件触发对象更新的问题,但是当对象更新比较耗时时(轮询触发也存在这个问题),我们还是会遇到对象更新时产生的卡顿问题:
using UnityEngine;
public class UpdateObject
{
public void Update()
{
// Update method may be very time cost,
// like "log using Debug"
Debug.Log("UpdateObject Update");
}
}
最普遍的解决方法就是优化 Update,使其耗时降低,但是很多情况下,这种方式也比较局限,更通用的一种方式则是分帧,即将对象的更新操作分摊到多帧中进行(而不是全部在一帧中执行完成).
实现分帧更新的方式很多,这里我使用了队列(并且做了进一步的优化,保证队列中的元素唯一):
using System.Collections.Generic;
using UnityEngine;
public class UniqueQueue<T>
{
Queue<T> m_innerQueue = new Queue<T>();
HashSet<T> m_elementSet = new HashSet<T>();
public int Count
{
get
{
Debug.Assert(m_innerQueue.Count == m_elementSet.Count);
return m_innerQueue.Count;
}
}
public bool Contains(T element)
{
return m_elementSet.Contains(element);
}
public T Dequeue()
{
if (Count > 0)
{
var element = m_innerQueue.Peek();
m_elementSet.Remove(element);
return m_innerQueue.Dequeue();
}
else
{
return default(T);
}
}
public bool Enqueue(T element)
{
if (!m_elementSet.Contains(element))
{
m_innerQueue.Enqueue(element);
m_elementSet.Add(element);
return true;
}
return false;
}
public void Clear()
{
m_innerQueue.Clear();
m_elementSet.Clear();
}
}
基于上述的 UniqueQueue, 我们便可以编写基于时间片(Timeslice)的对象更新了(即分帧进行对象更新):
using UnityEngine;
public class TimesliceUpdateManager : MonoBehaviour
{
[SerializeField]
float m_maxTimeslice = 1 / 30.0f;
UniqueQueue<UpdateObject> m_objectUniqueQueue = new UniqueQueue<UpdateObject>();
public void UpdateRequest(UpdateObject uo)
{
Debug.Assert(uo != null);
m_objectUniqueQueue.Enqueue(uo);
}
void Update()
{
if (m_objectUniqueQueue.Count > 0)
{
float elapsedTime = 0;
do
{
var curTime = Time.realtimeSinceStartup;
var uo = m_objectUniqueQueue.Dequeue();
uo.Update();
elapsedTime += Time.realtimeSinceStartup - curTime;
} while (elapsedTime < m_maxTimeslice && m_objectUniqueQueue.Count > 0);
}
}
}
TimesliceUpdateManager 的使用方法和上述的 UpdateManager 是一致的,但可以减少因为对象更新而引起的卡顿问题.