質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

Q&A

解決済

1回答

1765閲覧

Unity : Unirxで、5秒間のカウントダウンを繰り返し行えるボタンを実装したい

japomondo

総合スコア23

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

1グッド

2クリップ

投稿2018/01/26 13:46

編集2018/01/26 13:51

5秒間のカウントダウンを繰り返し行えるボタンを実装したいと考えています。

5,4,3,2,1,0と1秒毎に減っていき、0になると5に戻り、再度ボタンをクリックするとまた5からカウントダウンが始まる、という仕様です。

そこで下記のコードを書きました。

C#

1using System; 2using UnityEngine; 3using UnityEngine.UI; 4using UnityEngine.EventSystems; 5using UniRx; 6using UniRx.Triggers; 7 8public class PlayerControl : MonoBehaviour 9{ 10 11public bool IsRead; 12public Text remainingTime; 13public float time = 5.0f; 14 15void Awake () 16{ 17      this.UpdateAsObservable() 18            .Where(_ => IsRead) 19            .TakeWhile(_ => time > 0) 20            .Subscribe(_ => DiscountReadingTime()); 21} 22 23// ボタンを押すと呼ばれるメソッド 24public void Read () 25{ 26      IsRead = true; 27      StartCoroutine(EndReading()); 28} 29 30// Readメソッドが呼ばれることでUnirxにより走るメソッド 31void DiscountReadingTime () 32{ 33      time -= Time.deltaTime; 34      remainingTime.text = ((int)time).ToString(); 35} 36 37// Readメソッドから呼ばれるメソッド 38IEnumerator EndReading () 39{ 40      yield return new WaitForSeconds(time); 41      IsRead = false; 42 43      time = 5; 44      remainingTime.text = time.ToString(); 45} 46 47}

すると、DiscountReadingTimeメソッドについて問題が発生しました。1回目のクリックでは100%呼ばれるのですが、2回目以降になると呼ばれたり呼ばれなかったりします。ボタンを押してもカウントダウンが始まらないのです。

インスペクタでIsReadとtimeの値を確認したところ、いずれもDiscountReadingTimeメソッドが呼ばれる条件は満たしていました。

なお、ReadメソッドとEndReadingメソッドは2回目以降も100%呼ばれます。

TakeWhileは指定した条件が真である間値をプッシュし続けると認識しているのですが、Awakeメソッドに書いたUpdateAsObservableは、条件を満たしているはずなのになぜ実行されないのでしょうか?

もしお分かりになる方がいらっしゃいましたら、ご助言いただけますと大変幸いです。

asdfghjk👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

ちょっとご確認いただきたいのですが、コードを下記のようにすると、コンソールにはどのように表示されるでしょうか?

C#

1// 省略 2 3 void Awake() 4 { 5 this.UpdateAsObservable() 6 .Where(_ => IsRead) 7 .TakeWhile(_ => time > 0) 8 .Subscribe(_ => 9 { 10 Debug.LogFormat("time: {0}", time); 11 DiscountReadingTime(); 12 }, 13 () => Debug.LogFormat("Completed! time: {0}", time)); 14 } 15 16// 省略 17 18 // Readメソッドから呼ばれるメソッド 19 IEnumerator EndReading() 20 { 21 yield return new WaitForSeconds(time); 22 Debug.Log("Reset!"); 23 IsRead = false; 24 25 time = 5; 26 remainingTime.text = time.ToString(); 27 } 28 29// 省略

私の予想ですと、うまくいく場合では最後に「Reset!」だけが表示され、ボタンが無反応になる場合では先に「Completed! time:...」が表示されて、その後に「Reset!」が表示されるのではないでしょうか。

ちょっと調べた感じでは、TakeWhilefalseになるとそこでUpdateAsObservableのストリームは終了してしまい、以降timeIsReadを元に戻しても、ストリームがなくなっているので何も起こらない...という気がします。
想像するに、コルーチンによるリセットとTakeWhileによるストリーム終了判定が拮抗しており、先にリセットされた場合はうまくいくが、先にストリームが終了した場合は失敗するという状況ではないでしょうか?

改善案としては...

案1:
TakeWhileをやめて、time > 0でフィルタリングするだけにし、ストリームを止めないようにする。

C#

1// 省略 2 void Awake() 3 { 4 this.UpdateAsObservable() 5 .Where(_ => IsRead && time > 0) 6 .Subscribe(_ => DiscountReadingTime()); 7 } 8// 省略

案2:
Awakeは削除、EndReadingは通常のメソッドにし、Read内でストリーム購読を開始して、ストリーム完了時にEndReadingを実行する。

C#

1public class PlayerControl : MonoBehaviour 2{ 3 4 public bool IsRead; 5 public Text remainingTime; 6 public float time = 5.0f; 7 8 // ボタンを押すと呼ばれるメソッド 9 public void Read() 10 { 11 if (IsRead) 12 { 13 return; 14 } 15 16 IsRead = true; 17 this.UpdateAsObservable() 18 .TakeWhile(_ => time > 0) 19 .Subscribe(_ => DiscountReadingTime(), EndReading); 20 } 21 22 // Readメソッドが呼ばれることでUnirxにより走るメソッド 23 void DiscountReadingTime() 24 { 25 time -= Time.deltaTime; 26 remainingTime.text = ((int)time).ToString(); 27 } 28 29 // Readメソッドから呼ばれるメソッド 30 void EndReading() 31 { 32 IsRead = false; 33 time = 5; 34 35 if (remainingTime != null) 36 { 37 remainingTime.text = time.ToString(); 38 } 39 } 40}

案3:
案2をベースとしつつ、イベントストリームをUpdateAsObservableから1秒間隔のIntervalに置き換える(参考: UniRxのシンプルなサンプル その9(TimerとInterval 一定時間後に実行) - Qiita)。
timeは初期値5.0fのまま書き換えないようにし、ストリームに流れてくるプッシュ回数がtime未満の間テイクしてDiscountReadingTimeを実行する。
DiscountReadingTimeは引数としてプッシュ回数を受け取れるようにし、残り時間に換算してremainingTimeに表示する。
案2と同様、ストリーム完了時にEndReadingを実行する。

C#

1public class PlayerControl : MonoBehaviour 2{ 3 4 public bool IsRead; 5 public Text remainingTime; 6 public float time = 5.0f; 7 8 // ボタンを押すと呼ばれるメソッド 9 public void Read() 10 { 11 if (IsRead) 12 { 13 return; 14 } 15 16 IsRead = true; 17 Observable.Interval(TimeSpan.FromSeconds(1)) 18 .TakeWhile(i => i < time) 19 .Subscribe(DiscountReadingTime, EndReading); 20 } 21 22 // Readメソッドが呼ばれることでUnirxにより走るメソッド 23 void DiscountReadingTime(long eventIndex) 24 { 25 remainingTime.text = ((int)(time - (eventIndex + 1))).ToString(); 26 } 27 28 // Readメソッドから呼ばれるメソッド 29 void EndReading() 30 { 31 IsRead = false; 32 33 if (remainingTime != null) 34 { 35 remainingTime.text = time.ToString(); 36 } 37 } 38}

ずらずらと色々書いた後ですみませんが、恥ずかしながら私はUniRxについては名前しか聞いたことがなく(かなり便利で強力だという評判は目にしていたものの、リアクティブプログラミングに馴染みがなく手出ししていなかったのです)、ご質問者さんの方がお詳しいかと思います。
もし「こんな書き方だとこんな不具合があるよ」とか「こう修正するともっと効率的だよ」とかご指摘ありましたらぜひお願いします。

UniRxは多機能で十分理解するのに時間を要しそうですが、面白そうですね。もっと積極的に取り入れてみようかと思います。

投稿2018/01/26 21:52

編集2018/01/26 22:09
Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

japomondo

2018/01/27 02:41

またしてもご回答いただき、本当にありがとうございます。 早速冒頭のコードを試してみたところ、うまくいく場合では最初に「Completed! time:...」、最後に「Reset!」が表示され、ボタンが無反応になる場合では「Reset!」だけが表示される、という結果でした。 ご提案くださった案1を試させていただいたところ、何度クリックしてもカウントダウンされるようになりました。本当にありがとうございます。 Updateメソッドを呼び出すスクリプトを可能な限り少なくすることで負荷を減らしたいと思っていたところ、UniRxを知りその便利さに感動したのですが、自分などはまだ全然理解が浅く使いこなせていないので…。 ご提案いただいた案2、案3についても大変勉強になると思いますので、色々と試しつつ学びつつやってみます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問