如何精确采样频率为60Hz的数据?

How to precisely sample data with frequency of 60Hz?

实际上,我使用 InvokeRepeating 方法每 1/x 秒调用另一个方法。问题是调用和我得到的数据之间的延迟精度不好。

如何以 60Hz 的频率精确采样 transform.position

这是我的代码:

public class Recorder : MonoBehaviour {

public float samplingRate = 60f; // sample rate in Hz
public string outputFilePath;

private StreamWriter _sw;

private List<Data> dataList = new List<Data>();

public void OnEnable()
{

    InvokeRepeating("SampleNow", 0, 1 / samplingRate);
}

public void OnDisable()
{
    _sw = System.IO.File.AppendText(outputFilePath);

    for (int k=0; k< dataList.Count; k++)
    {
        _sw.WriteLine("t {0} x {1} y {2} z {3}",
           dataList[k].time, dataList[k].x, dataList[k].y, dataList[k].z);
    }

    _sw.Close();
    CancelInvoke();
}

public void SampleNow()
{
    dataList.Add(new Data(Time.time, transform.position.x, transform.position.y, transform.position.z));
}

public class Data
{
    public float time;
    public float x;
    public float y;
    public float z;

    public Data(float time, float x, float y, float z)
    {
        this.time = time;
        this.x = x;
        this.y = y;
        this.z = z;
    }
  }
}

所以,有了这个,我可以得到这样的结果:

t 0 x 0 y 0 z 0
t 0.02 x 0.283776 y -0.76 z 0
t 0.04 x 0.599808 y -0.52 z 0
t 0.06 x 0.599808 y -0.52 z 0
t 0.08 x 0.599808 y -0.52 z 0
t 0.09999999 x 0.599808 y -0.52 z 0
t 0.12 x 0.599808 y -0.52 z 0
t 0.14 x 0.599808 y -0.52 z 0
t 0.16 x 0.599808 y -0.52 z 0
t 0.18 x 0.599808 y -0.52 z 0
t 0.2 x 0.599808 y -0.52 z 0
t 0.22 x 0.599808 y -0.52 z 0
t 0.24 x 0.599808 y -0.52 z 0
t 0.26 x 0.599808 y -0.52 z 0
t 0.28 x 0.599808 y -0.52 z 0
t 0.3 x 0.599808 y -0.52 z 0
t 0.32 x 0.599808 y -0.52 z 0
t 0.3338465 x 0.599808 y -0.52 z 0
t 0.3338465 x 0.2918357 y -0.7538424 z 0
t 0.34 x 0.2918357 y -0.7538424 z 0
t 0.3539519 x 0.2918357 y -0.7538424 z 0
t 0.3539519 x 0.6092016 y -0.5125771 z 0
t 0.3705226 x 0.6092016 y -0.5125771 z 0
t 0.3870888 x 0.8340279 y -0.3137283 z 0
t 0.4036556 x 0.9750986 y -0.114934 z 0
t 0.42 x 0.9865224 y 0.09031145 z 0
...

正如你在这个结果中看到的,我可以收集不同时间(t=0.04 到 t=0.32)的重复位置,最糟糕的是,我可以在不同的位置(给出无限加速度)。

事实上,分析的游戏对象在时间上以线性函数移动,在 20 秒内沿 X-Y 轴做圆。

因此,此代码无法为我提供良好的科学分析精度。

如何使用 Unity3D 获得更高的精度?

这很复杂。虽然,我不会说在 Unity 中不可能。它可以精确地完成,但不能用 InvokeRepeating.

您还有其他两种方法可以做到这一点。

1.Coroutine

如果InvokeRepeating太慢,可能是使用了反射调用函数,也可能是实现不完善

您可以通过使用协程进行直接函数调用并等待 WaitForSecondsRealtime instead of WaitForSeconds. WaitForSecondsRealtime is new in Unity and does not depend on the frame-rate to wait. WaitForSeconds 来完成此操作。

public float samplingRate = 60f; // sample rate in Hz

void OnEnable()
{
    StartCoroutine(startSampling());
}

IEnumerator startSampling()
{
    WaitForSecondsRealtime waitTime = new WaitForSecondsRealtime(1f / samplingRate);
    while (true)
    {
        SampleNow();
        yield return waitTime;
    }
}

public void SampleNow()
{
    Debug.Log("Called");
    dataList.Add(new Data(Time.time, transform.position.x, transform.position.y, transform.position.z));
}

2.Thread(推荐)

我建议您使用这个,因为您可以完全避免 Unity 的主线程降低帧率。在另一个Thread.

中进行定时器操作

问题在于您无法在另一个线程中使用transform.position.xTransformclass。 Unity 会阻止您这样做。您必须在 Main Thread 中执行 dataList.Add(new Data(Time.time, transform.position.x...。另一种选择是将 transform.position 存储在全局 Vector3 变量中,然后从另一个 Thread 访问它。

不能也在另一个Thread中使用Time.time并且必须在主Thread中获取它,然后将它存储在将在 Thread 函数

中使用的局部变量

您还必须使用 lock 关键字使其 线程安全 。请注意,使用 lock 关键字会使您的游戏变慢 一点 位。它会从您的游戏中移除至少 2 或 3 帧,但它是必需的并且值得它提供的好处和保护。

下面的代码将完成我刚才所说的一切。出于测试目的,采样率设置为每秒 2 次。您可以在 samplingRate 变量中将它更改为您的 60Hz,当您认为它正在工作时 属性。

private List<Data> dataList = new List<Data>();

Thread samplingThread;
const float samplingRate = 2f; // sample rate in Hz

Vector3 posInThread;
float TimetimeInThread;

private System.Object threadLocker = new System.Object();


void OnEnable()
{
    startSampling();
}

void startSampling()
{
    samplingThread = new Thread(OnSamplingData);
    samplingThread.Start();
}

void Update()
{
    lock (threadLocker)
    {
        //Update this values to be used in another Thread
        posInThread = transform.position;
        TimetimeInThread = Time.time;
    }
}

void OnSamplingData()
{
    long oldTime = DateTime.Now.Ticks;
    long currentTime = oldTime;
    const float waitTime = 1f / samplingRate;
    float counter;

    while (true)
    {
        currentTime = DateTime.Now.Ticks;
        counter = (float)TimeSpan.FromTicks(currentTime - oldTime).TotalSeconds;

        if (counter >= waitTime)
        {
            //Debug.Log("counter: " + counter + "  waitTime: " + waitTime);
            oldTime = currentTime; //Store current Time
            SampleNow();
        }
        Thread.Sleep(0); //Make sure Unity does not freeze
    }
}

public void SampleNow()
{
    Debug.Log("Called");
    lock (threadLocker)
    {
        dataList.Add(new Data(TimetimeInThread, posInThread.x, posInThread.y, posInThread.z));
    }
}

void OnDisable()
{
    if (samplingThread != null && samplingThread.IsAlive)
    {
        samplingThread.Abort();
    }
    samplingThread.Join(); //Wait for Thread to finish then exit
}