FC2ブログ
ヽ|∵|ゝ(Fantom) の 開発blog? ホーム »Unityライブラリ
このページの記事一覧

【Unity】【C#】UniRx で「1フレームごと待機して処理」してみる  


 今まで「VRM Live Viewer」では非同期処理に「Await Extensions」というライブラリを使っていたのだが、「UniRx」の非同期処理関連の資料を見てたら、同じようなことができるとわかったので、移行してみようと考えた。


 ただ注意しなくてはならないのは、「UniRx」の非同期処理は Scripting Runtime を「.NET 4.x」にすることと、C# 7.0が必須のため、現時点(Unity2018.2.x)では「Incremental Compiler」を導入する必要があるとのことだ。

UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合

 「Incremental Compiler」はまだ Preview 版のため、商用利用は控えたほうが良いともあるね。まぁ、「VRM Live Viewer」はフリーなのと、色々実験してみたが、安定性にも問題無いようなので(どちらかというと Stable[安定版] が出たら、メソッドの仕様(引数など)が変わって、書き換え必須となることの方が辛いね(笑))、導入を試みてみたら成功した。実際「UniRx」上で使える「UniTask」は「async/await/Task」の上位互換で、現在私が使っている機能もそのまま移行できたので、基本的な使い方ならば、バージョンアップ・正式版が出てもそれほど問題ないとも思う。また今後UI処理などを少し強化したいしね。そういったものは「UniRx」はとても強い(笑)。

 なので、現時点では導入する人は少ないかも知れないが、「.NET 4.x」の非同期処理「async/await/Task」が使えるようになると、コルーチンで書いていたものがスッキリする上に、別スレッドでバックグラウンド処理などもとても簡単になるので、いずれは誰しも使うようになるだろう(笑)。そしてはじめに使いたいのはやはり、今までコルーチンなどで書いていた「1フレーム待機して処理」かなと(笑)。ググったらなぜかあまりハッキリとした答えがなかったので、ちょっと実験してみた感じ。たぶん他の人も同じことを調べるだろうしね。

■「1フレームごとに待機して処理」っぽくなりそうなものを、色々やってみる
 ●コルーチンで1フレームごとに待機して処理(ログ出力のみ。動作の基準)
 ●Observable.NextFrame() で1フレームごとに待機して処理?
 ●Observable.TimerFrame() で1フレームごとに待機して処理(引数=1)?
 ●Observable.TimerFrame() で1フレームごとに待機して処理(引数=0)?
 ●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=1)?
 ●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=0)?
 ●UniTask.DelayFrame() で1フレームごとに待機して処理?
 ●UniTask.WaitUntil() で1フレームごとに待機して処理?
■とりあえず「1フレームごとに待機して処理」を簡単に(実験してみた結果)

(※) Unity 2018.2.1f1 / UniRx 6.2.2 / Incremental Complier 0.0.42(Preview) / Windows10(x64) で確認



■「1フレームごとに待機して処理」っぽくなりそうなものを、色々やってみる

 まずは今まで通り、コルーチンで「1フレームごとに待機して処理」をしてみる。処理自体はただのログ出力なので、実際の処理の重さは考慮に入れてない。ちなみに「Await Extensions」では「yield return new WaitForEndOfFrame()」の代わりに「await new WaitForEndOfFrame()」が使える(そしてメソッドに async が使えるので非同期処理が簡潔に書ける。StartCoroutine() もいらない)。


●コルーチンで1フレームごとに待機して処理(ログ出力のみ。動作の基準)
using System.Collections;
using UnityEngine;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

StartCoroutine(WaitForEndOfFrameCoroutineTest());
}

IEnumerator WaitForEndOfFrameCoroutineTest()
{
Debug.Log("frame : " + Time.frameCount); //1
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //2
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //3
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //4
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //5
}
}

(Start) frame : 1
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5

 なんてことはないコルーチンで順次処理していくときのようなコード(本来は同じ処理ならループにするだろうが、これはあくまでサンプルなので、実際には別々の処理が入ると考えて欲しい)。これを基準として「UniRx」でオペレータなども使って色々試してみよう。なお、ここでは「yield return new WaitForEndOfFrame()」を使っているが、実行タイミングを気にしないなら「yield return null」でも良い。

イベント関数の実行順



●Observable.NextFrame() で1フレームごとに待機して処理?

using System.Collections;
using UnityEngine;
using UniRx;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

ObservableNextFrameTest();
}

async void ObservableNextFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //3
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //5
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //7
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //9
}
}

(Start) frame : 1
frame : 1
frame : 3
frame : 5
frame : 7
frame : 9

 UniRx で一番始めに目についたのは「Observable.NextFrame()」だったが、どうやら await で1フレーム、NextFrame() で1フレーム待機のようだ。フレームが1つ飛びになっていた。期待していた処理とは違う。



●Observable.TimerFrame() で1フレームごとに待機して処理(引数=1)?

using System.Collections;
using UnityEngine;
using UniRx;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

ObservableTimerFrameTest();
}

async void ObservableTimerFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //5
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //7
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //9
}
}

(Start) frame : 1
frame : 1
frame : 3
frame : 5
frame : 7
frame : 9

 Observable.NextFrame() と結果は同じになった。ではちょっと引数を0にしてみようと実験してみると…


●Observable.TimerFrame() で1フレームごとに待機して処理(引数=0)

using System.Collections;
using UnityEngine;
using UniRx;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

ObservableTimerFrameTest();
}

async void ObservableTimerFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //2
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //4
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //5
}
}

(Start) frame : 1
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5

 いけた(笑)。これは期待していた処理と合致する。



●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=1)?

using System.Collections;
using UnityEngine;
using UniRx;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

ObservableDelayFrameTest();
}

async void ObservableDelayFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //5
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //7
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //9
}
}

(Start) frame : 1
frame : 1
frame : 3
frame : 5
frame : 7
frame : 9

 これも Observable.NextFrame() と結果は同じになった。では引数を0にしてみると…


●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=0)

using System.Collections;
using UnityEngine;
using UniRx;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

ObservableDelayFrameTest();
}

async void ObservableDelayFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //2
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //4
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //5
}
}

(Start) frame : 1
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5

 これもいけた。これは期待していた処理と合致する。実装自体は Observable.TimerFrame() とは違うみたいだけどね。



●UniTask.DelayFrame() で1フレームごとに待機して処理?

using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Async;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

UniTaskDelayFrameTest();
}

async void UniTaskDelayFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //2
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //3
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //4
}
}

(Start) frame : 1
frame : 1
frame : 1
frame : 2
frame : 3
frame : 4

 せっかくのなで、UniTask も試してみた。するとあれ?なぜか初めの1回は待機されてない。なんか理由があるのだろうけど、今回は放おっておこう(←誰か調べて(笑))。こちらは「using UniRx.Async;」が必要。



●UniTask.WaitUntil() で1フレームごとに待機して処理?

using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Async;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

UniTaskWaitUntilTest();
}

async void UniTaskWaitUntilTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //2
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //3
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //4
}
}

(Start) frame : 1
frame : 1
frame : 1
frame : 2
frame : 3
frame : 4

 UniTask.DelayFrame() と同じ結果になった。これもはじめの1回が待機されてない…?ま、まぁ、今回はコルーチンでの表記をそのまま代替できる書き方を探していただけなので(パフォーマンスなども考慮に入れてない)、これで勘弁してやろう(笑)。誰かそのうち調べてくれるだろうと期待してる(←投げっぱなし(笑))。



■とりあえず「1フレームごとに待機して処理」を簡単に(実験してみた結果)

 他にも色々実験している記事もあったが、単純に面倒なので、とりあえず「Observable.TimerFrame(0)」または「Observable.ReturnUnit().DelayFrame(0)」を代替として使うことにしてみた。もしからしたら正しい使い方ではないかも知れないので、static な関数(WaitForFrame)にしておいて、後で書き換えられるようにしておけば、修正も楽かも知れない。「Observable.TimerFrame(0)」は UniRx 特有の「マイクロコルーチン」というものを使っているので(負荷が軽いらしい)、今回はこちらで書いておこう。まぁ、戻値の型の問題はあるが、あくまでコルーチンでの「yield return null」みたいな使い方を想定しているので、今回は気にしないとする(笑)。Observable のように拡張メソッドにしても良いと思うけど(「Observable_Joins.cs」に書くとか)、その辺はご自由に。

●とりあえず1フレームごとに待機して処理
using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Async;

public class EachFrameTest : MonoBehaviour {

// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1

WaitForFrameTest();
}

async void WaitForFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //2
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //3
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //4
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //5
}

public static IObservable<long> WaitForFrame()
{
return Observable.TimerFrame(0);
}
}

(Start) frame : 1
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5

 「Observable.TimerFrame()」だと戻値の型が long となっているので、Unit にしたいなら、以下のようにしても良いかも知れない(まぁ、今回の使い方のように、捨て値なら無駄な処理が増えるだけだが…)。

●戻値を Unit に変えた例
public static IObservable<Unit> WaitForFrame()
{
return Observable.TimerFrame(0).AsUnitObservable();
}


 こちらの記事にも書いてあるけど「コルーチンをほぼ駆逐」できると表記がすっきりし、しかもコルーチンと違って、別スレッド動作や非アクティブなオブジェクトでも動くので(というより、MonoBehaviour に依存しないで動作できるので)、いずれは「async/await」的な書き方の方が主流になるかもね。「VRM Live Viewer」は商用アプリにするつもりは無いし、実験的にやってみることに意義があると思ってるので(ファイルドロップ→非同期なファイル読み込みは既に実装されてる)、前のめりで新しい機能を導入していきたい(笑)。

(参考)
Unity UniRxとasync/awaitでフレーム管理
UniRx.Async機能紹介
UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合/a>
Unityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映





(関連記事)
【Unity】VRM(VRoid)をライブステージで踊らせるアプリを作ってみた


スポンサーサイト

category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityリファレンス  Unityライブラリ  実証実験 
tb: 0   cm: --

【Unity】【C#】UI-Text 版 FPS(フレームレート)をリアルタイムに測定して表示する  


 以前に作ったレガシーGUI を uGUI(UI.Text)に移植しただけのもの。

 StandardAssets には「FPSCounter」という FPS測定ライブラリも入っているが、ソースを見てみると、一定間隔の平均を求めてるようなので、できればリアルな(平均でない)値を見たいと思って、わざわざ移植した(と言ってもほとんどまるっとコピーだが(笑))。

(※) Unity 5.6.3p1 - 2018.2.1f1 / Windows10(x64) で確認



●UI-Text 版 リアルタイムにFPSを測定して表示する
using System.Text;
using UnityEngine;
using UnityEngine.UI;

// FPS 測定値を UI-Text に表示する
// http://fantom1x.blog130.fc2.com/blog-entry-307.html
public class FpsText : MonoBehaviour {

public Text targetText; //表示する UI-Text
public string displayFormat = "{0:F1} FPS"; //表示フォーマット(F0 とすれば小数点以下は無くなる)

//測定用
int tick = 0; //フレーム数
float elapsed = 0; //経過時間
float fps = 0; //フレームレート

StringBuilder sb = new StringBuilder(16);

// Use this for initialization
private void Start () {
if (targetText == null)
targetText = GetComponentInChildren<Text>();
}

// Update is called once per frame
private void Update () {
tick++;
elapsed += Time.deltaTime;
if (elapsed >= 1f) {
fps = tick / elapsed;
tick = 0;
elapsed = 0;

if (targetText != null)
{
sb.Length = 0;
sb.AppendFormat(displayFormat, fps);
targetText.text = sb.ToString();
}
}
}
}

 使い方は UI の Text などにアタッチし、targetText に表示する Text をセットすれば完了だ(未セットでも子要素までは自動で探す)。

 測定(計算)方法は Update() による Time.deltaTime で集計した単位時間あたりのフレーム数である。あくまでもフレーム更新での測定なので、現実時間に近い測定(連続した時間)にしたいなら Time.realtimeSinceStartup で測定するのも良いだろう(StandardAssets の FPSCounter は Time.realtimeSinceStartup での測定)。

 実は色々な FPSカウンターを見てみると、測定(計算)方法はまちまちなので、用途に合わせた選択をした方が良いだろう。例えば StandardAssets の「FPSCounter」では連続的な時間での平均的な fps 測定に、今回の「FpsText」では一定更新毎(連続的というより、更新約1秒ごとにフレーム数を測定という感じ)の測定には向いてるかも知れない。具体的に言えば、平均化されてる場合は、もたったりして、ある程度バラついてもフラットな値になるが、平均化されてない場合は、もたったりしてる瞬間は値が激変する。自分がどちらの値を欲してるかで決めれば良い。

 以前の記事にも書いたが完全にフレームごとの超詳細な測定をしたいなら、毎フレーム「1f / Time.deltaTime」で計算する方法もある。ただしこの場合、ほんの僅かなもたりでも(人間の感覚ではわからないくらい小さいものでも)値として出るので、通常使う分には向いてないかも知れない(また、物凄く更新が速いため、見づらいという欠点もある)。仕様はケースバイケースで合わせた方が良いだろう。


※この記事のスクリプトはプラグインのライブラリにも同梱されています。


※とりあえず試してみたい方には、最新版をビルドした apk デモをダウンロードできます。動作確認にもどうぞ。

プラグインデモをダウンロード
(Google Drive を利用)


Android 4.2以上
※「提供元不明アプリのインストール」許可が必要です。


(関連記事)
【Unity】【C#】FPS(フレームレート)をリアルタイムに測定して表示する
【Unity】【C#】FPS(フレームレート)をリアルタイムに測定して表示するv2(4隅選択可能で、画面サイズの変更にも対応版)


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityライブラリ  C# 
tb: 0   cm: --

【Unity】【C#】インスペクタでの UnityEvent のコールバック登録の有無を調べる  


 利用頻度はあまり高くはないかもだが、インスペクタでの UnityEvent のコールバック登録があるか否かを調べたいときがある。実際に利用するには調べたい条件によってコードを変えるしかないかも知れないが、簡単な例を2つばかり挙げておこう。また、UnityEvent にはジェネリックパラメタの数違いのパターンがあるので、それらをカバーするためのオーバーロードも簡単に作っておく。

 ここでは汎用的に使えるように、拡張メソッドとして定義したものを書いておこう。ライブラリとして保存しておけば使い回しもできると思う。拡張メソッドにしたくない場合は、メソッドの引数の this を取り除けば良い。


(※) Unity 5.6.3p1 - 2018.1.8f1 / Windows10(x64) で確認



■インスペクタの UnityEvent にコールバックが1つでも登録されてるか否かを返す

 このメソッドたちは1つでもコールバック名が有るか無いかという判別をするものである。UnityEvent が null のときは常に false になる。ジェネリックパラメタの数違いはオーバーロードとして定義している。

●インスペクタの UnityEvent にコールバックメソッドが存在するか否かを返す(関数定義)
using System;
using UnityEngine;
using UnityEngine.Events;

public static class Extensions //※クラス名は任意
{
//インスペクタの UnityEvent にコールバックが1つでも登録されてるか否かを返す。
//※AddListener() で登録したものは無視されるので注意。
public static bool HasRegistered(this UnityEventBase obj)
{
if (obj != null)
{
int count = obj.GetPersistentEventCount(); //※AddListener() には無効
for (int i = 0; i < count; i++)
{
if (!string.IsNullOrEmpty(obj.GetPersistentMethodName(i))) //※AddListener() には無効
return true;
}
}
return false;
}

//※以下、パラメタ違いオーバーロード
public static bool HasRegistered(this UnityEvent obj)
{
return HasRegistered((UnityEventBase)obj);
}

public static bool HasRegistered<T0>(this UnityEvent<T0> obj)
{
return HasRegistered((UnityEventBase)obj);
}

public static bool HasRegistered<T0, T1>(this UnityEvent<T0, T1> obj)
{
return HasRegistered((UnityEventBase)obj);
}

public static bool HasRegistered<T0, T1, T2>(this UnityEvent<T0, T1, T2> obj)
{
return HasRegistered((UnityEventBase)obj);
}

public static bool HasRegistered<T0, T1, T2, T3>(this UnityEvent<T0, T1, T2, T3> obj)
{
return HasRegistered((UnityEventBase)obj);
}
}

●使用例(メインでのコードなど)
using UnityEngine;

Debug.Log("OnNothing.HasRegistered = " + OnNothing.HasRegistered());
Debug.Log("OnSingle.HasRegistered = " + OnSingle.HasRegistered());
Debug.Log("OnDouble.HasRegistered = " + OnDouble.HasRegistered());
Debug.Log("OnTriple.HasRegistered = " + OnTriple.HasRegistered());
Debug.Log("OnQuadruple.HasRegistered = " + OnQuadruple.HasRegistered());


//コールバックハンドラなど
public void ReceiveNothing()
{
Debug.Log("ReceiveNothing called.");
}

public void ReceiveSingle(string s)
{
Debug.Log("ReceiveSingle called.");
}

public void ReceiveDouble(string s, int i)
{
Debug.Log("ReceiveDouble called.");
}

public void ReceiveTriple(string s, int i, float f)
{
Debug.Log("ReceiveTriple called.");
}

public void ReceiveQuadruple(string s, int i, float f, bool b)
{
Debug.Log("ReceiveQuadruple called.");
}

● False になる例

OnNothing.HasRegistered = False
OnSingle.HasRegistered = False
OnDouble.HasRegistered = False
OnTriple.HasRegistered = False
OnQuadruple.HasRegistered = False

● True になる例

OnNothing.HasRegistered = True
OnSingle.HasRegistered = True
OnDouble.HasRegistered = True
OnTriple.HasRegistered = True
OnQuadruple.HasRegistered = True

 注意して欲しいのは「GetPersistentEventCount()」や「GetPersistentMethodName()」というメソッドを使っているが、これらは「AddListener()」でランタイム時に追加したメソッドには無効ということだ。なので、基本的にはインスペクタでの登録のみと考えて欲しい(タイトルが「インスペクタでの~」となってるのはそのため)。

 また、メソッドの存在は「GetPersistentMethodName()」で空か否かで判断しているだけなので、何らかミスなどで "<Missing ~>"(※ここではわざと無効な Dummy() メソッドを登録している)となっているメソッドも存在していると判断されてしまうので注意しよう。しかし通常、インスペクタでの登録は正常であるという前提なら問題はない。



■インスペクタの UnityEvent にシグニチャが合致しているコールバックが1つでも登録されてるか否かを返す

 次のメソッドたちは前述のコールバック名の存在有無より厳密に、コールバックの引数のシグニチャと同じものだけを検出するものである。実際インスペクタでの登録は違うシグニチャも登録できるわけで(例:コールバック引数が(string s, int i)であっても、引数が(string s)[※インスペクタで文字列を入力] のメソッドを登録することができる)、便利ではあるが前述の方法では正しくコールバックできるメソッドが存在するかは判定できないことにもなる。以下の方法では完全に正しいシグニチャを持つメソッドしか検出しないので、"<Missing ~>"(※ここではわざと無効な Dummy() メソッドを登録している)となっているものも排除できる。

●インスペクタの UnityEvent に引数が正しいコールバックメソッドが存在するか否かを返す(関数定義)
using System;
using System.Reflection;
using UnityEngine;
using UnityEngine.Events;

public static class Extensions //※クラス名は任意
{
//インスペクタの UnityEvent にシグニチャが合致しているコールバックが1つでも登録されてるか否かを返す。
public static bool HasValidMethod(this UnityEventBase obj, Type[] argumentTypes)
{
if (obj != null)
{
int count = obj.GetPersistentEventCount(); //※AddListener() には無効
for (int i = 0; i < count; i++)
{
string method = obj.GetPersistentMethodName(i); //※AddListener() には無効
if (!string.IsNullOrEmpty(method))
{
object target = obj.GetPersistentTarget(i); //※AddListener() には無効
MethodInfo info = UnityEventBase.GetValidMethodInfo(target, method, argumentTypes);
if (info != null)
return true;
}
}
}
return false;
}

//※以下、パラメタ違いオーバーロード
public static bool HasValidMethod(this UnityEvent obj)
{
return HasValidMethod(obj, new Type[]{});
}

public static bool HasValidMethod<T0>(this UnityEvent<T0> obj)
{
return HasValidMethod(obj, new Type[]{typeof(T0)});
}

public static bool HasValidMethod<T0, T1>(this UnityEvent<T0, T1> obj)
{
return HasValidMethod(obj, new Type[]{typeof(T0), typeof(T1)});
}

public static bool HasValidMethod<T0, T1, T2>(this UnityEvent<T0, T1, T2> obj)
{
return HasValidMethod(obj, new Type[]{typeof(T0), typeof(T1), typeof(T2)});
}

public static bool HasValidMethod<T0, T1, T2, T3>(this UnityEvent<T0, T1, T2, T3> obj)
{
return HasValidMethod(obj, new Type[]{typeof(T0), typeof(T1), typeof(T2), typeof(T3)});
}
}

●使用例(メインでのコードなど)
using UnityEngine;

Debug.Log("OnNothing.HasValidMethod = " + OnNothing.HasValidMethod());
Debug.Log("OnSingle.HasValidMethod = " + OnSingle.HasValidMethod());
Debug.Log("OnDouble.HasValidMethod = " + OnDouble.HasValidMethod());
Debug.Log("OnTriple.HasValidMethod = " + OnTriple.HasValidMethod());
Debug.Log("OnQuadruple.HasValidMethod = " + OnQuadruple.HasValidMethod());

//・・・コールバックハンドラなどは前述と同じで良いので省略・・・

● False になる例

OnNothing.HasValidMethod = False
OnSingle.HasValidMethod = False
OnDouble.HasValidMethod = False
OnTriple.HasValidMethod = False
OnQuadruple.HasValidMethod = False

● True になる例

OnNothing.HasValidMethod = True
OnSingle.HasValidMethod = True
OnDouble.HasValidMethod = True
OnTriple.HasValidMethod = True
OnQuadruple.HasValidMethod = True

 使い分けは大まかに「何でも良いからメソッドが登録されてる」ときと「正確に値を返せるメソッドが登録されてる」となるだろう。どちらを使うかはケースバイケースだと思う。


(関連記事)
【Unity】【C#】UnityEvent, Action, delegate, interface でのコールバック実装方法とインスペクタでの登録
【Unity】【C#】長押し(ロングタップ)を取得してコールバックする
【Unity】【C#】スワイプ(フリック)を判定、方向を取得してコールバックする
【Unity】【C#】ピンチ操作を取得してコールバックする
【Unity】【C#】VideoPlayer で動画の終了判定をする


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityライブラリ  Unityリファレンス  C# 
tb: 0   cm: --

【Unity】【C#】動的にオーディオファイルの読み込みと再生をする  


 プラグイン ver.1.16 の簡易音楽プレイヤーを作る際にちょっと調べたんだけど、VideoPlayer と違って、基本的に WWW を使うのが常套みたいだね。プラグインのデモスクリプトとしては「EasyMusicPlayer.cs」と「ExternalStorageTest2.cs」の2種類で書かれているが、基本は同じなので再利用しやすい部分を抜粋して書いておくことにした。実際に使う際にはもう少し手を加えた方が便利かも知れない。



(※) Unity 5.6.3p1 - 2018.1.8f1 / Windows10(x64) で確認



■オーディオファイルの読み込みと再生のコード

●オーディオファイルの読み込みと再生
using System.Collections;
using System.IO;
using UnityEngine;

public class AudioLoadTest : MonoBehaviour //※クラス名は任意
{
public AudioSource audioSource; //インスペクタで AudioSource をセット
public string path = "/storage/emulated/0/Music/sample.mp3"; //※ファイルは任意

//外部からの呼び出し用メソッド
public void LoadAudio(string path)
{
if (Path.GetExtension(path) == ".m4a") //※"m4a"は再生できないっぽい
{
Debug.Log("Not supported audio format.");
return;
}

StartCoroutine(LoadToAudioClipAndPlay(path));
}

//ファイルの読み込み(ダウンロード)と再生
IEnumerator LoadToAudioClipAndPlay(string path)
{
if (audioSource == null || string.IsNullOrEmpty(path))
yield break;

if (!File.Exists(path)) {
//ここにファイルが見つからない処理
Debug.Log("File not found.");
yield break;
}

using(WWW www = new WWW("file://" + path)) //※あくまでローカルファイルとする
{
while (!www.isDone)
yield return null;

AudioClip audioClip = www.GetAudioClip(false, true);
if (audioClip.loadState != AudioDataLoadState.Loaded)
{
//ここにロード失敗処理
Debug.Log("Failed to load AudioClip.");
yield break;
}

//ここにロード成功処理
audioSource.clip = audioClip;
audioSource.Play();
Debug.Log("Load success : " + path);
}
}

// Use this for initialization
private void Start () {
//起動時に読み込むときなど
LoadAudio(path); //※メインからの呼び出し例
}
}

 ここではインスペクタに path を入力し読み込むようになっているが、外部呼び出し用のメソッド「LoadAudio()」の引数にパスを渡すようになっているので、通常は「Start()」内の呼び出しは削除した方が良いだろう(ただのメインからの呼び出し例のため)。

 オーディオファイルの読み込みには WWW を使っているが、あくまでローカルストレージを想定しているので、頭に "file://" を付けているのに注意して欲しい。インターネットからダウンロードしたい場合は "file://" を取り除いて URL形式("http://~")を直接与えれば良いと思う。しかしその場合、タイムアウトを付けたり、キャッシュ付きなどを使う必要があるかも知れない(パケ代もかかるので)。

 WWW を使っている分にはオーディオ形式は気にしなくても良いが、再生するには "mp3" や "ogg", "wav" などが良いようだ。"m4a" は読み込み(ダウンロード)はできるが、再生はできなかった(Unity2018.1.5f1時点)。

 スマホで使うにはアクセス権などが必要になることもあるが、とりあえずはこれだけで読み込みと再生はできる。以降では更にプラグインを利用して、自由にファイルを選択して読み込む方法もやってみよう。



■スマホ(Android)のストレージから読み込んで再生する

 ここではプラグインを使ってストレージを開き、再生する方法を書いておこう。といってもスクリプト自体は上記の例そのままで良いので、後はプレファブをシーンに置き、コールバックをセットするだけだ。プラグインのインストールからはじめる場合は、以前の記事を参考にして欲しい。


1.先に作った「AudioLoadTest.cs」スクリプトを適当な GameObjectにアタッチし、「AudioSource」を追加し、インスペクタでセットドラッグ&ドロップでセットしよう。「Play On Awake」はオフに、「Loop」はオンにしておくと良い(※曲を繰り返し再生する場合)。



2.シーンにプレファブ「StorageOpenAudioController」(~/FantomLib/Prefabs/System/ 以下。検索で探すと楽)を置き、インスペクタで「Mime Types」に読み込むオーディオ形式の MIME type を入れておく。ここでは mp3/m4a = "audio/mpeg", ogg = "audio/ogg","application/ogg" を入れているが、任意で良い(※デフォルト=空の状態ではすべてのオーディオ形式となる)。




 なお、ogg の MIME type が2種類書かれているが、プロバイダ(ストレージ)の種類によって、同じ拡張子でも MIME type が違ってたりするので注意が必要だ。例えばローカルストレージでは "application/ogg" となり、GoogleDrive などでは "audio/ogg" で認識される。今回の再生の場合、ローカルストレージの "application/ogg" だけでも良い(複数指定して置けば、どのプロバイダ[ストレージ]でも見えるようになる)。直接開けるか否かはアプリの対応状況によるため、実際に使う際にはあらかじめ絞り込んでおくのも良いだろう。

※ここで言う「プロバイダ」とはアプリが持つ情報提供プログラムのことである。Android アプリはそれぞれアプリごとにプロバイダを持つことができ、それによりユーザーに与える情報を制限することができる。この例の場合、ローカルストレージや GoogleDrive(これは1つのアプリである)、OneDrive(これもアプリ)ごとということになるので、「ストレージ」ごとになることと同義になる。


3.シーンにロード用のボタンを置き、コールバック「OnClick」に「StorageOpenAudioController.Show」を登録する。これでボタンを押すことにより、ストレージが開く。






4.プレファブ「StorageOpenAudioController」に戻り、「OnResult」に先に作ったスクリプト「AudioLoadTest.LoadAudio」を登録する(※手順2の画像のようになる)。これでストレージから選択したオーディオファイルのパスがスクリプトに渡され、読み込みが成功すれば再生される(※"m4a" は再生できないようなので(Unity2018.1.5f1 時点)、LoadAudio() で拡張子チェックをしてるが、実際に使うには拡張子で絞るようにしても良い。「EasyMusicPlayer.cs」(~/FantomLib/Scripts/Example/ 以下)にはその例も書かれているので必要なら参照)。なお、GoogleDrive などクラウドストレージからは直接再生(情報取得)できないので注意。その場合は一旦ローカルストレージにコピー(ダウンロード)すれば再生できるようになる。これで読み込みと再生だけなら完成だ。


※実機でログを確認するにはシーンに「DebugConsole」(~/FantomLib/Prefabs/System/ 以下)を置き、「AudioLoadTest.cs」内の「Debug.Log()」→「XDebug.Log()」にしておく。ビルドするには「デモのビルド」と基本的に同じ(シーンだけ変える)なので必要なら参照。


 あとは「プレイ/停止」など操作できるとなお良いね。このあたりのサンプルはデモシーン「ExternalStorageTest」の「ExternalStorageTest2.cs」に、またはシーン「MusicPlayerExample」の「EasyMusicPlayer.cs」(~/FantomLib/Scripts/Example/ 以下)に書かれているので、コピペしても良いと思う。「EasyMusicPlayer.cs」には連続再生やシャッフル、次の曲/前の曲、プレイリストに追加/削除など、一般的な操作の例を入れてあるので、必要なら参照して欲しい(最低限の機能で良いなら、そのまま使っても良い→「UnityChanInOtakuCity」はそのまま使ってる)。



※他、様々な機能を使ったデモが同梱。Unity×Androidアプリ開発に!

※サンプルで使っているループ曲を含んだライブラリも配信中。全31曲!


※とりあえず試してみたい方は、最新版をビルドした apk デモをダウンロードできます。動作確認にもどうぞ。

プラグインデモをダウンロード
(Google Drive を利用)


Android 4.2以上
※「提供元不明アプリのインストール」許可が必要です。


(関連記事)
【Unity】AssetStore版 FantomPlugin のセットアップ
【Unity】Androidのトーストやダイアログ、通知、音声認識、ハード音量操作など基本的な機能を使えるプラグインを作ってみた
【Unity】【C#】VideoPlayer で動画の終了判定をする


このコンテンツの一部には『ユニティちゃんライセンス』で許諾されたアセットが使用されています。
Portions of this work are licensed under Unity-Chan License


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityライブラリ  Unityプラグイン  C# 
tb: 0   cm: --

【Unity】【C#】配列・リストのシャッフル  


 簡易音楽プレイヤーのデモを作っていて気づいたんだけど、C# にはシャッフル(Shuffle())の標準メソッドは無いのね。Java には Collections.shuffle() のメソッドがあるので、つい探してしまった…。あと要素のスワップ(Swap())も Java には標準であるんだよね(Collections.swap())。ちなみに私はプラグイン開発してるときは Java と C# を同時に書いているので、たまに混同する(笑)(実際に AndroidStudio とVisualSutdio の両方を開いている)。まぁ簡単に作れるものだけど、毎回書くのも面倒なのでとりあえずメモ的に残して置こうと思った。



(※) Unity 5.6.3p1 - 2018.1.5f1 / Windows10(x64) で確認



■配列のシャッフル

●配列のシャッフル(UnityEngine.Random / 拡張メソッド版)
using UnityEngine;
using Random = UnityEngine.Random;

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
//配列の要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this T[] arr)
{
for (int i = arr.Length - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
T tmp = arr[i]; //swap
arr[i] = arr[j];
arr[j] = tmp;
}
}
}


//メインでは・・・
using System.Linq;

int[] arr = { 0, 1, 2, 3, 4, 5 };
for (int i = 0; i < 5; i++)
{
arr.Shuffle();
Debug.Log(arr.Select(e => e.ToString()).Aggregate((s, a) => s + ", " + a));
}

4, 3, 1, 5, 2, 0
5, 4, 3, 2, 0, 1
0, 3, 5, 1, 2, 4
1, 5, 0, 2, 4, 3
0, 1, 2, 4, 3, 5

 使っているアルゴリズムは有名な「フィッシャー–イェーツのシャッフル」(英:Fisher–Yates shuffle)である。詳細は Wikipedia を見て欲しいが、簡単に言えばインデクス(i)を後ろから1つずつ前方へ移動しながら、0~i の範囲(終点は[1](と[0]))でランダムに要素をスワップするアルゴリズムである。オーダーは O(n) なので巨大な配列で毎フレーム更新とかではなければ(笑)、それほど負荷は無いだろう。音楽プレイヤーではどの様に使ってるかというと、曲順を1度シャッフルして、その順番に再生し、最後まで行ったらまたシャッフル…を繰り返しているだけである。なので、ほとんど負荷は無いと言って良い。そういう使い方の場合、何の問題も無いと考えて良いだろう(Unityの様な毎フレーム更新のアプリケーションはなるべくオーダーの小さいアルゴリズム[O(1)とかO(logn)とか]を採用する方が良い)。

 またついでなので要素のスワップも定義して置くと、以下のように簡潔に書ける。

●配列のシャッフル+要素のスワップ(UnityEngine.Random / 拡張メソッド版)
using UnityEngine;
using Random = UnityEngine.Random;

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
//配列の要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this T[] arr)
{
for (int i = arr.Length - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
arr.Swap(i, j);
}
}

//要素のスワップ
public static void Swap<T>(this T[] arr, int i, int j)
{
T tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}

//メインは同じ(※略)

 なお、ここでは拡張メソッドを用いているが、通常のメソッドで書くと以下のようになる。

●配列のシャッフル(UnityEngine.Random / 通常メソッド版)
・・・(略)・・・
public class Extensions //※通常メソッドの場合、static でなくても可
{
//配列の要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(T[] arr)
{
for (int i = arr.Length - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
Swap(arr, i, j);
}
}

//要素のスワップ
public static void Swap<T>(T[] arr, int i, int j)
{
T tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}

//メインでは・・・
・・・(略)・・・
Extensions.Shuffle(arr); //※クラス名は任意の定義による
・・・(略)・・・

拡張メソッドは便利だが、増えすぎるとかえって邪魔になることもあるので、汎用性の高いものだけにすると良いだろう。

 あと、ここでは Unity用にランダム関数は UnityEngine.Random を使っているが、C#標準の System.Random を使いたい場合、以下のように書くこともできる。

●配列のシャッフル+要素のスワップ(System.Random / 拡張メソッド)
using UnityEngine;
using Random = System.Random; //ここでエイリアスにしておくと表記が楽

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
static Random rand = new Random(); //※毎回 new するならいっそ static で利用する

//配列の要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this T[] arr)
{
for (int i = arr.Length - 1; i > 0; i--)
{
int j = rand.Next(i + 1); //[0]~[i]
arr.Swap(i, j);
}
}

//スワップは拡張メソッドと同じ(※略)
}

//メインは同じ(※略)

 System.Random の場合は毎回 new しなくてはならないので、いっそ static で使い回しした方が良いだろう。もちろん「Shuffle<T>(this T[] arr, Random rand)」のように引数にランダムジェネレータを与えるようにしても良いと思う(他の言語を含め、標準関数にはそういうものが多い)。

 が、まぁしかし、Unity を使っている分には「UnityEngine.Random」の方が簡単だし、円状とか球状とか便利なので迷わず使っても良いだろう。また数が少ない場合、UnityEngine.Random の方が良い感じに乱数が出るという話もある。こういった実験は私もよくやるのだが、情報を公開してくれていると非常に有り難い(笑)。なので私も複数の書き方やアルゴリズムを試したりしてるわけだ(といってもアルゴリズム系の記事は Java の方が多いが[※思いつきでやるのでたまたま])。以下に参考資料も載せておこう。

(参考)Unityの乱数が本当にランダムかどうか見てみた



■リストのシャッフル

●リストのシャッフル(UnityEngine.Random / 拡張メソッド版)
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
//リストの要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this List<T> list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
T tmp = list[i];
list[i] = list[j];
list[j] = tmp;
}
}
}


//メインでは・・・
using System.Linq;

List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5 };
for (int i = 0; i < 5; i++)
{
list.Shuffle();
Debug.Log(list.Select(e => e.ToString()).Aggregate((s, a) => s + ", " + a));
}

4, 3, 1, 5, 2, 0
5, 4, 3, 2, 0, 1
0, 3, 5, 1, 2, 4
1, 5, 0, 2, 4, 3
0, 1, 2, 4, 3, 5

 やっていることは配列のシャッフルと変わらないので、説明はいらないだろう。一瞬、IEnumerable で書くことも頭をよぎったが、そもそもシャッフルの対象となるのは静的配列かリストが最も多い。特に拡張メソッドで書くと入力候補もどんどん増加してしまうので、用途が限定されるならこれで十分と思った。まぁ、必要なら好きに改造してくれたまい(笑) [※たぶんコードが煩雑になる]。

 リストのスワップ定義も配列と同じようにやってみると、随分簡潔になる。

●リストのシャッフル+要素のスワップ(UnityEngine.Random / 拡張メソッド版)
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
//リストの要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this List<T> list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
list.Swap(i, j);
}
}

//要素のスワップ
public static void Swap<T>(this List<T> list, int i, int j)
{
T tmp = list[i];
list[i] = list[j];
list[j] = tmp;
}
}

//メインは同じ(※略)

 もちろん、配列のときと同じように通常のメソッドで書くには以下のようになる。

●リストのシャッフル(UnityEngine.Random / 通常メソッド版)
・・・(略)・・・
public class Extensions
{
//リストの要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(List<T> list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
Swap(list, i, j);
}
}

//要素のスワップ
public static void Swap<T>(List<T> list, int i, int j)
{
T tmp = list[i];
list[i] = list[j];
list[j] = tmp;
}
}

//メインでは・・・
・・・(略)・・・
Extensions.Shuffle(list); //※クラス名は任意の定義による
・・・(略)・・・

 ついでに C#標準の System.Random を使う場合、配列のときと同じように以下のように書ける。

●配列のシャッフル+要素のスワップ(System.Random / 拡張メソッド)
using UnityEngine;
using Random = System.Random; //ここでエイリアスにしておくと表記が楽

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
static Random rand = new Random(); //※毎回 new するならいっそ static で利用する

//リストの要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this List<T> list)
{
for (int i = arr.Length - 1; i > 0; i--)
{
int j = rand.Next(i + 1); //[0]~[i]
arr.Swap(i, j);
}
}

//スワップは拡張メソッドと同じ(※略)
}

//メインは同じ(※略)

 これも配列のときと同じように「Shuffle<T>(this List<T> list, Random rand)」のように引数にランダムジェネレータを与えるようにしても良いと思う。だがまぁ、「UnityEngine.Random」の方を使った方が楽だろう。

 ちなみに配列版の方が2倍くらい速いので、動的に要素数を変更する必要がないのなら、配列を用いた方が良いだろう(※基本的に配列と List で全く同じアルゴリズムでやってみると、配列の方が2~3倍ほど処理が速い)。


 ゲームの場合、アイテムドロップに直接ランダム関数を用いることも多いと思うが、アイテムのインデクスの確率テーブルなどを作ってシャッフルすれば、全体としては確実に分布が行き渡るので、そういう使い方も良いかもね(例えばテーブルを {0,0,0,1,1,2} としてシャッフル→順番に取り出せば、確実に 3:2:1 の分布でアイテムが出る=直接ランダムの場合、回数が多くなるほど確率は正しくなるが、少ない回数では偏ることもあるため)。

 音楽プレイヤーのシャッフルなんかも直接ランダムの場合、曲数少ないと「A-B-A-A-B-C」(=3曲を6回ランダム再生)のように偏ることもあるが、シャッフルで {A,C,B}+{C,A,B} のようにすれば(=3曲を2回シャッフルで順に再生)偏りは減る(末尾と先頭が連続することはあるが、全体的にはバラバラ順に聞こえる)。プラグイン(ver.1.16以降)の簡易音楽プレイヤーでもそのまま実装しているので、興味があったらダウンロードしてビルドして試してみるのも良いだろう(※好きに改造して貰っても構わない)。


※他、様々な機能を使ったデモが同梱。Unity×Androidアプリ開発に!

※サンプルで使っているループ曲を含んだライブラリも配信中。全31曲!




(関連記事)
【Unity】AssetStore版 FantomPlugin のセットアップ
【C#】連想配列(Dictionary)を値 or キーでソート
【C#】2次元配列(ジャグ配列・多次元配列)のソート
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など
【Java】配列要素の反転(reverse)


このコンテンツの一部には『ユニティちゃんライセンス』で許諾されたアセットが使用されています。
Portions of this work are licensed under Unity-Chan License


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityライブラリ  C#ライブラリ  アルゴリズム 
tb: 0   cm: --


プロフィール

Social

検索フォーム

全記事一覧

カテゴリ

ユーザータグ

最新記事

リンク

PR

▲ Pagetop