4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Unityでms単位で周期的に発行し続けようとして諦めた話。:Unityで、ある程度安定して周期的に発行し続ける方法。

Posted at

参考記事

ZenjectとUniRx.MessageBrokerの相性が抜群
C#6.0時代のUnity

環境

Unity 2017.3.1
.NET 4.6(c#6.0)

経緯

かなり短めに伝えると、

起:Code: Transcendence by reminisce
こういうものが作りたい。
MessageBrokerを使って、一定周期毎に発行するストリームを用いて音楽の同期を取れば良いのではないか?

承:フレーム単位よりも細かく、ms単位の精度でMessageBrokerを一定間隔でPublishしたい

転:MessageBrokerは良くないらしいのでSubjectで作成・ms単位は無理っぽいのでframe単位で安定性を重視
↑いまここ

結:完成してGitHubにあげる。
  

ということで、ms単位の精度を求めること自体が間違いなのではないかという結論に達するまでの試行錯誤の記事です。

失敗集

Observable.Interval

今回はUniRxとZenjectを用いることが前提であり、まずObservable.Intervalでmsを指定してやれば簡単に行けるんじゃないかなと思ってました。

Interval.cs
void Start()
{
    float start_time, previous_time = Time.realtimeSinceStartup;

    Observable.Interval(TimeSpan.FromMilliseconds(10))
        .Subscribe(_ =>
        {
            start_time = Time.realtimeSinceStartup;
            //10msではなく、1frame待つことになる。
            Debug.Log($"Interval time:{start_time - previous_time}");
            previous_time = start_time;
        }).AddTo(this);
}

これでいけると思っていたのですが、実際に動かしてみると
Interval.JPG
10ms毎に発行するのではなく、frame単位(1/60秒)で動作していました。
その他色々な方法・Coroutine・Taskについても実施しましたが、これらもframe単位の動作であり、msレベルの待機や発行が実施できるものではありませんでした。

Coroutine

Coroutine.cs
    bool isDestroyed = true;
    void Start()
    {
        StartCoroutine("MsCoroutine");
    }
    private IEnumerator MsCoroutine()
    {
        float start_time, previous_time = Time.realtimeSinceStartup;
        while (true)
        {
            //10msではなく、1frame待つことになる。
            yield return new WaitForSeconds(0.01f);
            start_time = Time.realtimeSinceStartup;
            Debug.Log($"Interval time:{start_time - previous_time}");
            previous_time = start_time;
        }
    }
    private void OnDestroy()
    {
        isDestroyed = true;
    }

Coroutine.JPG
Coroutineだと1frameずつ動く感じ。

Task

Task.cs
    bool isDestroyed = true;
    void Start()
    {
        MsTask();
    }
    async void MsTask()
    {
        float start_time, previous_time = Time.realtimeSinceStartup;
        while (!isDestroyed)
        {
            //1frame + α待つことになる。
            await Task.Delay(10);
            start_time = Time.realtimeSinceStartup;
            Debug.Log($"Interval time:{start_time - previous_time}");
            previous_time = start_time;
        }
    }

Task.JPG
Taskだと基本1frameで、貯まると2frame待つ感じになるのかな。
5frameで16.6ms分貯まるってのが良く分からないけれども。

結論

FixedUpdate

結局の所、ms単位で実施は無理っぽいので、だったら確からしさを重視して作成……となるとFixedUpdateが一番良いのかなというのが今の所の結論です。
MonoBehaviour.FixedUpdate()

MonoBehaviour が有効の場合、この関数は毎回、固定フレームレートで呼び出されます。

固定フレームレートで呼ばれるという点ですが、これは少し注意が必要です。
固定フレームレートというのは、例えばFixedUpdateが1秒に50回実施する場合はラグ等で1秒48回になったとしても次の1秒を52回実施することで補填するというぐらいの意味で認識しておいてください。
また、このFixedupdateの実施タイミングはUpdateの方の実行タイミングに依存します。
簡単に言うと、例えばfixedUpdateが50fpsでUpdateが60fpsだったら、fixedUpdateの実行タイミングは下記画像のようなIntervalになります。
Task.JPG

今回はFixedUpdateを使用していますが、より精度を高めるためにFixedTimestepをデフォルトの0.02ではなく0.01で実施しています。

BeatConductor.cs
    float _startTime, _nextTickTime;
    void Start()
    {
        _startTime = 0;
        //1Tickあたりの時間を代入する。
        _nextTickTime = (float)_tempo.SecondsPerTick();
        _tickReactive
            //始めの1小節は不安定なので発行しない。
            .Skip(_tempo.TickPerTuplet * 8)
            .Subscribe(_ =>
        {
            _Timing.Tick++;
            if (_Timing.Tick % _tempo.TickPerTuplet == 0)
            {
                _Timing.Tuplet++;
                if (_Timing.Tuplet % _tempo.TupletPerBeat() == 0)
                {
                    _Timing.Tick = 0;
                    _Timing.Beat++;
                    if (_Timing.Beat % _tempo.BeatPerBar() == 0)
                    {
                        _Timing.Bar++;
                        _Timing.Tuplet = 0;
                        _Timing.Beat = 0;
                        if (_Timing.Bar % 4 == 0)
                        {
                            if (_Timing.Bar == 0)
                            {
                                _nextTickTime -= _startTime;
                                _startTime = 0;
                            }
                            //4小節毎に発行する。
                            _rhythmObserver.FourBars.OnNext(new TimingForFourBars(_Timing));
                        }
                        //1小節毎に発行する。
                        _rhythmObserver.Bar.OnNext(new TimingForBar(_Timing));
                    }
                    //1beat毎に発行する。
                    _rhythmObserver.Beat.OnNext(new TimingForBeat(_Timing));
                }
                //連符毎に発行する。
                _rhythmObserver.Tuplet.OnNext(new TimingForTuplet(_Timing));
            }
            //Tick毎に発行する。
            _rhythmObserver.Tick.OnNext(new TimingForTick(_Timing));
        }).AddTo(this);
    }
    private void FixedUpdate()
    {
        _startTime += Time.deltaTime;
        if(_startTime > _nextTickTime - (Time.deltaTime / 2))
        {
            _tickReactive.OnNext(Unit.Default);
            //1Tickあたりの時間を追加する。
            _nextTickTime += (float)_tempo.SecondsPerTick();
        }
    }
Receiver.cs
private void Start()
{
    float first_time = Time.realtimeSinceStartup;
    //1小節毎に購読する。
    _rhythmReceiver.Bar
        .Subscribe(_ =>
        {
            Debug.Log($"Bar:{_.Bar},TotalTime:{Time.realtimeSinceStartup - first_time}");
        }).AddTo(this);
}

BeatConductor.JPG
BPM120で4分の4拍子なので、1小節あたり2秒間隔で購読できているとOK。
fixedUpdate.JPG

これでOKだと考えています。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?