この記事では、Unityを使用してマルチスレッドモバイルゲームを開発する際に発生する主な問題と、 UniRx (Unityのリアクティブ拡張)を使用してそれらを解決する方法について説明します。
この記事は2つの部分で構成されています。 1つ目は「最小」のマルチスレッドに特化しており、アクセス可能な言語でスレッドとスレッドの作成方法、スレッドの同期について説明しています。 2番目の部分は、リアクティブエクステンション、その配置、動作原理、および適用方法に専念します。
Unityでスクリプトを記述するための言語の1つはアプリケーションを開発するC#であるため、すべてのコードはその上でのみ記述されます。 マルチスレッドおよびリアクティブ拡張の原則を深く理解するには、マルチスレッドの基本とリアクティブ拡張とは何かを読むことをお勧めします。 読者がこのトピックに精通している場合、最初のセクションはスキップできます。
最小のマルチスレッド
マルチスレッドアプリケーションは、複数のタスクを別々のスレッドで同時に実行するアプリケーションです。 マルチスレッドを使用するアプリケーションは、ユーザーインターフェイスがアクティブのままであるため、ユーザーのアクションにより迅速に応答しますが、集中的なプロセッサー作業を必要とするタスクは他のスレッドで実行されます。 Monoを使用するC#マルチスレッドアプリケーションは、キーワードThread、ThreadPool、および非同期デリゲートを使用して開発されます。
構成例を使用して、マルチスレッドアプリケーションを見てみましょう。 各労働者が他の労働者と同時にその職務を遂行すると仮定します。 たとえば、1つは床を洗い、もう1つは窓を洗います。 (そして、これはすべて同時に起こります)。 これらは私たちの流れです。
スレッド-既存のアプリケーション内に新しいスレッドを作成できるクラス。
非同期デリゲート-呼び出されたメソッドと同じシグネチャで定義されたデリゲートを使用して、メソッドを非同期的に呼び出します。 非同期メソッド呼び出しの場合、BeginInvokeメソッドを使用する必要があります。 このアプローチでは、デリゲートはプールからストリームを取得し、その中のコードを実行します。
ThreadPool-「オブジェクトプール」パターンの実装。 その意味は、効果的なフロー管理::作成、削除、作業の割り当てです。 建設の例えに戻ると、ThreadPoolは建設現場の建設業者の数を管理し、それぞれにタスクを割り当てる職長です。
スレッド同期ツール
C#言語は、スレッドを同期するためのツールを提供します。 これらのツールは、ロックおよびモニターの形式で表示されます。 これらは、コードブロックの実行が複数のスレッドによって同時に実行されないようにするために使用されます。 ただし、注意点が1つあります。 これらのツールを使用すると、デッドロック(デッドロックスレッド)につながる可能性があります。 これは次のように発生します。スレッドAはスレッドBが制御を返すのを待ち、スレッドBはスレッドAがブロックされたコードを実行するのを待ちます。 したがって、マルチスレッドとスレッドの同期は注意して使用する必要があります。
UnityのUnityスレッドの問題
シングルスレッドアプリケーションの開発時に直面する主な問題は、メインスレッドでの複雑な操作に起因するUIフリーズです。 Unityにはタスクを並列化するメカニズムがあり、コルーチン(コルーチン)の形式で表示されますが、1つのスレッドで動作し、コルーチンで「重い」何かを実行する場合-こんにちは、フリーズ。 メインスレッドで関数の並列実行に満足している場合は、コルーチンを使用できます。 複雑なことは何もありません。Unityのドキュメントでは、このトピックは非常によくカバーされています。 ただし、コルーチンはUnityで次のように動作するイテレーターであることを思い出してください。
- 最初のステップはコルチンを登録することです
- さらに、 Updateを呼び出すたびにLateUpdateを呼び出す前に、Unityは登録されているすべてのコルーチンをポーリングし、メソッド内に記述されているコードを処理します。
長所に加えて、コルーチンには欠点もあります。
- 戻り値を取得できません
private IEnumerator LoadGoogle() { var www = new WWW("http://google.com"); yield return www; // www.text . }
- エラー処理
private IEnumerator LoadGoogle() { try { var www = new WWW("http://google.com"); yield return www; } catch { yield return null; } }
- コールバック付き松葉杖
private IEnumerator LoadGoogle(Action<string> callback) { var www = new WWW("http://google.com"); yield return www; if (callback != null) { callback(www.text); } }
- コルーチンで重いメソッドを処理しないでください
void Start() { Debug.Log(string.Format("Thread id in start method = {0}", Thread.CurrentThread.ManagedThreadId)); StartCoroutine(this.HardMethod()); } private IEnumerator HardMethod() { while (true) { Thread.Sleep(1001); Debug.Log(string.Format("Thread id in HardMethod method = {0}", Thread.CurrentThread.ManagedThreadId)); yield return new WaitForEndOfFrame(); } } //Output: //Thread id in start method = 1 //Thread id in HardMethod method = 1 //Thread id in HardMethod method = 1 //Thread id in HardMethod method = 1
前述のように、コルーチンはメインスレッドで機能します。 このため、重いメソッドを起動してフリーズを取得します。
これらの欠点の多くは、リアクティブエクステンションの助けを借りて簡単に解消できます。リアクティブエクステンションは、将来、さらに多くの異なる機能をもたらし、開発を容易にします。
リアクティブ拡張とは何ですか?
リアクティブ拡張機能は、Linqスタイルのイベントと非同期呼び出しを操作できるライブラリのセットです。 このような拡張機能の目標は、非同期の相互作用が現れるコードの記述を単純化することです。 Unityは、基本的なリアクティブ拡張機能を提供するUniRxライブラリを使用します。 UniRx-.NET Reactive Extensionsに基づくUnityのリアクティブエクステンションの実装。 なぜこのネイティブ実装を使用できないのですか? Unityの標準RXが機能しないためです。 ライブラリはクロスプラットフォームであり、PC / Mac / Android / iOS / WP8 / WindowsStoreプラットフォームでサポートされています。
UniRxは何を提供しますか?
- マルチスレッド
- LINQのようなメソッド
- 簡素化された非同期相互作用構文
- クロスプラットフォーム
どのように機能しますか?
リアクティブエクステンションのコアは、
IObserver
および
IObservable
です。 それらは、オブザーバー設計パターンとしても知られるプッシュ通知の一般的なメカニズムを提供します。
- IObservableインターフェイスは、通知(プロバイダー)を送信するクラスを表します。
IObserverインターフェースは、それらを受け取るクラス(オブザーバー)を表します。
Tは、通知用の情報を提供するクラスを表します。
IObserver実装は、インスタンスをIObservable.Subscribe
プロバイダーIObservable.Subscribe
渡すことにより、プロバイダー(IObservable実装)から通知を受信する準備をします。 このメソッドは 、プロバイダーが通知の送信を完了する前にオブザーバーの登録を解除するために使用できるIDisposableを返します 。
IObserverインターフェイスは、オブザーバーが実装する必要がある次の3つのメソッドを定義します。
- OnNextメソッド。通常、オブザーバーに新しいデータまたはステータス情報を提供するためにプロバイダーによって呼び出されます。
- OnErrorメソッド。通常、プロバイダーによって呼び出され、データにアクセスできない、破損している、またはプロバイダーに他のエラーがあることを示します。
- OnCompletedメソッド。通常は、オブザーバーへの通知送信の完了を確認するためにプロバイダーによって呼び出されます。
UniRxは、マルチスレッドが実装されるメインコンポーネントであるスケジューラも実装します。 UniRxの基本的な一時操作(間隔、タイマー)は、MainThreadを使用して実装されます。 これは、ほとんどの操作(Observable.Start
を除く)がメインスレッドで動作することを意味し、この場合、スレッドセーフは無視できます。Observable.Start
はデフォルトでThreadPool Schedulerを使用します。つまり、スレッドが作成されます。
基本概念と理論的知識に精通したので、UniRxライブラリの使用例を検討します。
オブザーバーを作成する例
この例では、UniRxライブラリを使用してインターネットリソースからデータを取得しようとします。 リアクティブ拡張機能を使用してデータをダウンロードするには、オブザーバーを作成し、標準のWWW
UnityクラスのラッパーであるObservableWWW
クラスを使用する必要があります。Get
メソッドGet
コルーチンを使用し、オブザーバーをサブスクライブするIObservableを返します。 このアプローチにより、「Unityの組み込みマルチスレッドメカニズムの問題」セクションで説明した松葉杖が回避されます。
private void Start() { var observer = Observer.Create<string>( x => { Debug.Log("OnNext: " + x); }, ex => Debug.Log("OnError: " + ex.Message), () => Debug.Log("OnCompleted")); ObservableWWW.Get("http://qweqweqwe.qwer.qwer/").Subscribe(observer); } //Output: //OnError: Exception of type 'UniRx.WWWErrorException' was thrown.
http://www.nixsolutions.com/などの適切なリンクにリンクを変更すると、次の結果が得られます。
//Output: //OnNext: ”html ” //OnCompleted
サブジェクトシーケンスの作成例
ここでは、2つのDebug.Logsにサインアップしました。1つ目はOnNext
メソッドがOnNext
されたときに常に実行され、2つ目は条件に応じてのみトリガーされます。
void Start() { this.subject = new Subject<int>(); this.subject.Subscribe(x => Debug.Log(x)); this.subject.Where(x => x % 2 == 0).Subscribe(x => Debug.Log(string.Format("Hello from {0}", x))); } // Update is called once per frame void Update() { this.sub.OnNext(this.i++); } //Output: //0 //Hello from 0 //1 //2 //Hello from 2
EveryUpdateの例
これらの拡張機能の重要な機能は、EveryUpdate
メソッドです。 UpdateメソッドおよびMonoBehaviour
下位MonoBehaviour
からコードをMonoBehaviour
できます。 ここでは、マウスのクリックをチェックし、テキストを表示します。
Observable.EveryUpdate() .Where(x => Input.GetMouseButton(buttonIndex)) .Subscribe(x => Debug.Log(outputString)); //Output: //Left button pressed //Right button pressed
配列の例
これらの拡張機能のもう1つの興味深い機能は、配列の処理です。 以下に示すコードは、シングルスレッドアプリケーションで実行されると、表示ストリームをフリーズします。
var arr = Enumerable.Range(0, 5); foreach (var i in arr) { Thread.Sleep(1000); Debug.Log(string.Format("Result = {0}, UtcNow = {1}, ThreadId = {2}", i, DateTime.UtcNow, Thread.CurrentThread.ManagedThreadId)); } //Output: //Result = 0, UtcNow = 8/25/2015 1:23:14 PM, ThreadId = 1 //Result = 1, UtcNow = 8/25/2015 1:23:16 PM, ThreadId = 1 //Result = 2, UtcNow = 8/25/2015 1:23:17 PM, ThreadId = 1 //Result = 3, UtcNow = 8/25/2015 1:23:18 PM, ThreadId = 1 //Result = 4, UtcNow = 8/25/2015 1:23:19 PM, ThreadId = 1
この問題を解決するには、スレッド間で負荷を分散するThreadPoolスケジューラーを明示的に指定して、UniRxを使用できます。
var arr2 = Enumerable.Range(0, 5).ToObservable(Scheduler.ThreadPool); arr2.Subscribe( x => { Thread.Sleep(1000); Debug.Log(string.Format("Result = {0}, UtcNow = {1}, ThreadId = {2}", x, DateTime.UtcNow, Thread.CurrentThread.ManagedThreadId)); }); //Output: //Result = 0, UtcNow = 8/25/2015 1:23:20 PM, ThreadId = 2 //Result = 1, UtcNow = 8/25/2015 1:23:21 PM, ThreadId = 3 //Result = 2, UtcNow = 8/25/2015 1:23:22 PM, ThreadId = 4 //Result = 3, UtcNow = 8/25/2015 1:23:23 PM, ThreadId = 5 //Result = 4, UtcNow = 8/25/2015 1:23:24 PM, ThreadId = 5
このアプローチの興味深い特徴は、フリーストリームが各反復に割り当てられることです。
複雑なメソッド処理の例
この例では、メインスレッドで実行されるとアプリケーションをフリーズする複雑なメソッドがいくつかあります。 Rxを使用すると、すべてが正常に機能し、メソッドから戻り値を取得して処理します。
private void Awake() { var heavyMethod = Observable.Start(() => { var timeToSleep = 1000; var returnedValue = 10; Debug.Log(string.Format("Thread = {0} UtcNow = {1}", Thread.CurrentThread.ManagedThreadId, DateTime.UtcNow)); Thread.Sleep(timeToSleep); return returnedValue; }); var heavyMethod2 = Observable.Start(() => { var timeToSleep = 2000; var returnedValue = 20; Debug.Log(string.Format("Thread = {0} UtcNow = {1}", Thread.CurrentThread.ManagedThreadId, DateTime.UtcNow)); Thread.Sleep(timeToSleep); return returnedValue; }); Observable.WhenAll(heavyMethod, heavyMethod2) .ObserveOnMainThread() .Subscribe(result => { Debug.Log(string.Format("Thread = {0}, first result = {1}, second result = {2} UtcNow = {3}", Thread.CurrentThread.ManagedThreadId, result[0], result[1], DateTime.UtcNow)); }); } //Output: //Thread = 5 UtcNow = 8/25/2015 2:06:55 PM //Thread = 3 UtcNow = 8/25/2015 2:06:55 PM //Thread = 1, first result = 10, second result = 20 UtcNow = 8/25/2015 2:06:57 PM
バインダーの使用例
別の優れたメカニズムはバインディングです。 これを使用すると、MVPパターンを簡単に実装できます。 この例では、モデルはEnemyクラスであり、反応クラスを記述しています。IsDead
プロパティはCurrentHp
IsDead
依存します。ゼロ未満の場合、IsDead
は= trueになります。
public class Enemy { public Enemy(int initialHp) { this.CurrentHp = new ReactiveProperty<long>(initialHp); this.IsDead = this.CurrentHp.Select(x => x <= 0).ToReactiveProperty(); } public ReactiveProperty<long> CurrentHp { get; private set; } public ReactiveProperty<bool> IsDead { get; private set; } }
プレゼンターは、モデルとディスプレイの接続を担当します。これにより、モデルの反応特性をディスプレイのパーツにバインドできます。MvpExample
クラスはプレゼンターであり、モデル(Enemy
クラス)とディスプレイ(Button
およびToggle
)の両方へのリンクがあります。 また、リアクティブエクステンションのおかげで、コードを使用してさまざまなUI要素の動作を設定することができます。OnClickAsObservable
およびOnValueChangedAsObservable
を使用して、ButtonおよびToggleの動作を説明しました。
public class MvpExample : MonoBehaviour { private const int EnemyHp = 1000; [SerializeField] private Button myButton; [SerializeField] private Toggle myToggle; [SerializeField] private Text myText; private void Start() { var enemy = new Enemy(EnemyHp); this.myButton.OnClickAsObservable().Subscribe(x => enemy.CurrentHp.Value -= 99); // CurrentHp Enemy this.myToggle.OnValueChangedAsObservable().SubscribeToInteractable(this.myButton); // Toggle enemy.CurrentHp.SubscribeToText(this.myText); enemy.IsDead.Where(isDead => isDead) .Subscribe(_ => { this.myToggle.interactable = this.myButton.interactable = false; }); } }
次に、UI要素にリアクティブプロパティを追加しました。Enemy
がCurrentHp
変更すると、テキストも自動的に変更されます。IsDead
状態がtrue
変わると、ボタンとToggle
両方が無効になります。
結論
Unityでアプリケーションを開発するときにリアクティブエクステンションを使用すると、多くの利点があります。 主なものは、マルチスレッドアプリケーションを構築するための構文を簡素化することです。 コルーチンを使用した松葉杖の数が大幅に削減され、アプリケーションの柔軟性と高速性が向上します。 また、UniRxを使用してマルチスレッドアプリケーションを構築する場合、複数のストリームによる値の変更からデータの一部を保護する必要があることに注意する必要があります。
便利なリンク: