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

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

新規登録して質問してみよう
ただいま回答率
85.35%
Unity

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

Q&A

解決済

3回答

1804閲覧

位置ベクトル間のPingPong

退会済みユーザー

退会済みユーザー

総合スコア0

Unity

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

0グッド

0クリップ

投稿2021/01/30 17:51

編集2021/02/10 18:52

前提・実現したいこと

位置ベクトル間で、PingPongメソッドを使って往復運動を実装させるにはどのようにすればよいでしょうか?
そもそもPingPongメソッドで、位置ベクトル間の往復運動が実装可能かどうかということがわかっていないです。
やりたいこととしては、キャラクタの往復運動です。
PingPongメソッドで実装可能であれば、おそらく1番手軽な実装になると思うのでPingPongメソッドで
実装したいと考えています。

試したこと

Vector3にPingPongメソッドはありませんでした。
Mathf.PingPongメソッドしかなさそうです。

y軸座標に関しては、現状のゲームオブジェクトの位置を維持という意図で下記のように組んでみました。

C#

1 float startTime; 2 float speed = 20; 3 Vector3 dir; 4 5 void Start() 6 { 7 startTime = Time.time; 8 dir = endPoint.transform.position - startPoint.transform.position; 9 } 10 11 void Update() 12 { 13 float dx = Mathf.PingPong((Time.time-startTime)*speed, Mathf.Abs(dir.x)); 14 float dz = Mathf.PingPong((Time.time-startTime)*speed, Mathf.Abs(dir.z)); 15 if (dir.x < 0) dx = -dx; 16 if (dir.z < 0) dz = -dz; 17 transform.position = new Vector3(startPoint.transform.position.x+dx, 18 transform.position.y, startPoint.transform.position.z+dz); 19 }

上記でゲームを実行したところ、往復運動はするものの、直線上の動きではなく、
カクカクとした動きとなってしまいました。
この不具合の原因は、x座標とz座標とで、dirの最大値に到達するタイミングが合わないためと考えています。
そうすると、PingPongメソッドで、位置ベクトル間での往復運動は実装不可能なのでしょうか?

ちなみに、dx=-dxのような処理をしているのは、
PingPongメソッドの第2引数が負の値に対応していないみたいだからです。
以前の質問は、「負の値も振り幅にする」であって、
片道方向ならば、もしかしたら、第2引数で負の値に対応しているのかもしれないと期待したのですが、
ダメそうでした。下記のようなログを取ってみました。
負の値に対応してなさそうというのはわかるのですが、なぜ一律に-4の出力なるのかは不明です。

C#

1 Debug.Log(Mathf.PingPong(0, -2)); // -4 2 Debug.Log(Mathf.PingPong(1, -2)); // -4 3 Debug.Log(Mathf.PingPong(2, -2)); // -4 4 Debug.Log(Mathf.PingPong(3, -2)); // -4

また、位置ベクトル間の往復運動が実装できたとして、
ゲームオブジェクトをY軸周りで、進行方向に向かせたいのですが、どのように実装すればよいでしょうか?
Y軸周りで向きを設定することに関しては、進行方向のベクトルをdirとすると、

C#

1this.transform.rotaion = Quaternion.Euler(0, dir.y, 0);

で設定できると考えていて、
進行方向は、startPointかendPointのどちらかから、求めることができることはわかっていますが、
現在、startPointとendPointのどちらに向かっているかを取得する方法は、
まず、PingPongメソッドの往復運動が実装できてからでないと、考えることができないという状態です。

追記

・length - Mathf.Repeat(t, length * 2.0f)

C#

1 int length = 3; 2 for (int t=0; t<13; t++){ 3 Debug.Log($"Mathf.PingPong({t},{length}){Mathf.PingPong(t,length)}, 4 length - Mathf.Repeat(t, length * 2.0f){length - Mathf.Repeat(t, length * 2.0f)}"); 5 }

出力結果。

Mathf.PingPong(0,3):0, length - Mathf.Repeat(t, length * 2.0f):3 Mathf.PingPong(1,3):1, length - Mathf.Repeat(t, length * 2.0f):2 Mathf.PingPong(2,3):2, length - Mathf.Repeat(t, length * 2.0f):1 Mathf.PingPong(3,3):3, length - Mathf.Repeat(t, length * 2.0f):0 Mathf.PingPong(4,3):2, length - Mathf.Repeat(t, length * 2.0f):-1 Mathf.PingPong(5,3):1, length - Mathf.Repeat(t, length * 2.0f):-2 Mathf.PingPong(6,3):0, length - Mathf.Repeat(t, length * 2.0f):3 Mathf.PingPong(7,3):1, length - Mathf.Repeat(t, length * 2.0f):2 Mathf.PingPong(8,3):2, length - Mathf.Repeat(t, length * 2.0f):1

追記②

レイの変更箇所だけ記載します。

C#

1 if (Physics.Raycast(new Ray(new Vector3(newPosition.x, 100.0f, newPosition.z), Vector3.down), out var hitInfo)) 2 { 3 if(!(hitInfo.collider.CompareTag("Player") || hitInfo.collider.CompareTag("Ground"))){ 4 newPosition = hitInfo.point; 5 } 6 Debug.Log(hitInfo.collider); 7 }

イメージ説明

追記③

C#

1 IEnumerator Turn(Transform trf){ 2 Vector3 baseForward = trf.forward; 3 for (float time = 0.0f; time < this.animationLength; time += Time.deltaTime) 4 { 5 float t = time / this.animationLength; 6 Quaternion q = Quaternion.Euler(0.0f, Mathf.Lerp(0.0f, 180.0f, t), 0.0f); 7 trf.forward = q * baseForward; 8 yield return null; 9 } 10 // 補間値t=1の調整処理 11 trf.forward = Quaternion.Euler(0.0f, 180.0f, 0.0f) * baseForward; 12 }

追記④

C#

1 IEnumerator YTurnToTarget(Transform trf, Transform target){ 2 Vector3 dir = (target.position - trf.position).normalized; 3 Debug.Log(dir.y); 4 Vector3 baseForward = trf.forward; 5 for (float time = 0.0f; time < this.animationLength; time += Time.deltaTime) 6 { 7 float t = time / this.animationLength; 8 Quaternion q = Quaternion.Euler(0.0f, Mathf.Lerp(0.0f, dir.y, t), 0.0f); 9 trf.forward = q * baseForward; 10 yield return null; 11 } 12 // 補間値t=1の調整処理 13 trf.forward = Quaternion.Euler(0.0f, dir.y, 0.0f) * baseForward; 14 }

追記⑤

C#

1 float startTime; 2 float speed = 2.0f; 3 4 void Update(){ 5 if (Input.GetMouseButtonDown(0)){ 6 // TimetimeCoroutineが動いていたら何もしないという処理を可能であれば書きたい 7 startTime = Time.time; 8 StartCoroutine(TimeCoroutine(startTime)); 9 }else if(Input.GetMouseButtonDown(1)){ 10 Suspension(TimeCoroutine, DoSomethingCoroutine); 11 } 12 } 13 14 // Time.timeを使ったコルーチン 15 // 今回はPingPongの処理に該当。コードが長くなり、他でエラーが発生しているため、暫定的な内容となりました。 16 IEnumerator TimeCoroutine(float startTime){ 17 float t = (Time.time - startTime) * speed; 18 while(true){ 19 Debug.Log("$TimeCoroutine:{t}"); 20 } 21 } 22 23 IEnumerator DoSomethingCoroutine(float animationLength){ 24 for (float time = 0.0f; time < animationLength; time += Time.deltaTime) 25 { 26 float t = time / this.animationLength; 27 // 補間値tを使った何かしらの処理 28 Debug.Log($"t:{t}"); 29 yield return null; 30 } 31 // 補間値t=1の調整処理 32 Debug.Log("t:1"); 33 } 34 35 void Suspension(Coroutine TimeCoroutine, Coroutine SuspensionCoroutine){ 36 // 第1引数のTimetimeCoroutineが動いていなければ何もしない 37 // if文で判定してreturn 38 // ...というような処理を組みたいと思ったのですが、コルーチンの生存確認をする方法が 39 // 見つかりませんでした。 40 // フィールドにフラグを持たせて、TimetimeCoroutine内で切り替えるという手法以外にありますか? 41 StopCoroutine(TimeCoroutine(0f)); 42 Debug.Log("中断"); 43 float pauseTime = Time.time; 44 yield return SuspensionCoroutine(); 45 startTime += Time.time - pauseTime; 46 StartCoroutine(TimeCoroutine(startTime)); 47 Debug.Log("再開"); 48 }

追記⑥

C#

1public class MyTime : MonoBehaviour 2{ 3 private static bool m_isActive = false; 4 private static float m_time; 5 private static float m_timeScale = 1.0f; 6 private static IEnumerator mytimeCoroutine; 7 public static float time{ 8 get { return m_time; } 9 } 10 public static bool isActive{ 11 get { return m_isActive; } 12 } 13 14 public static float timeSale{ 15 get { return m_timeScale; } 16 set { 17 if (m_timeScale == 0 && m_isActive){ 18 StopCoroutine(mytimeCoroutine); 19 m_timeScale = 0; 20 }else{ 21 m_timeScale = value; 22 } 23 } 24 } 25 26 private static IEnumerator TimeCoroutine(){ 27 // 本当はメソッドのようにreturnで処理を中止したかったのですが、できませんか? 28 // if (m_isActive) return; 29 if(!m_isActive){ 30 m_isActive = true; 31 while(true){ 32 m_time += Time.deltaTime; 33 m_time *= m_timeScale; 34 yield return null; 35 } 36 } 37 } 38 39 public static void StartTime(){ 40 StartCoroutine(TimeCoroutine()); 41 } 42 43 public static void StopTime(){ 44 if (!m_isActive) return; 45 m_isActive = false; 46 StopCoroutine(mytimeCoroutine); 47 } 48 49 public static void RestartTime(){ 50 if (mytimeCoroutine == null) return; 51 StartCoroutine(mytimeCoroutine); 52 } 53 54 public static void ClearTime(){ 55 StopTime(); 56 mytimeCoroutine = null; 57 m_time = 0; 58 } 59}

追記⑦

C#

1public class TestCoroutine : MonoBehaviour 2{ 3 4 UnityEngine.Coroutine RunningMyCoroutine; 5 6 void Start() 7 { 8 RunningMyCoroutine = StartCoroutine(MyCoroutine()); 9 } 10 11 void Update() 12 { 13 if(Input.GetMouseButtonDown(0)){ 14 RunningMyCoroutine = StartCoroutine(MyCoroutine()); 15 } 16 } 17 18 IEnumerator MyCoroutine(){ 19 if (RunningMyCoroutine != null) yield break; 20 int i = 0; 21 while(i<5){ 22 Debug.Log($"MyCoroutine:{i}"); 23 i++; 24 yield return new WaitForSeconds(2.0f); 25 } 26 RunningMyCoroutine = null; 27 } 28}

追記⑧

C#

1 [SerializeField] GameObject startPoint; 2 [SerializeField] GameObject endPoint; 3 [SerializeField] float animationLength = 1.0f; 4 5 readonly float speed = 4; 6 7 void Start() 8 { 9 Vector3 startPosition = startPoint.transform.position; 10 Vector3 endPosition = endPoint.transform.position; 11 startPosition.y = 0.0f; 12 endPosition.y = 0.0f; 13 StartCoroutine(Move(this.transform, endPosition)); 14 } 15 16 IEnumerator Move(Transform trf, Vector3 toPosition) 17 { 18 WaitForInterruption interrupter = new WaitForInterruption(); 19 while (true) 20 { 21 Vector3 currentPosition = trf.position; 22 Vector3 nextPosition = Vector3.MoveTowards(currentPosition, toPosition, speed * Time.deltaTime); 23 if (currentPosition == nextPosition) 24 { 25 break; 26 } 27 trf.position = nextPosition; 28 // yield return new WaitUntil(()=>true); 29 // yield return new WaitWhile(()=>false); 30 // yield return false; 31 // yield return true; 32 yield return interrupter; 33 } 34 } 35 36 class WaitForInterruption : CustomYieldInstruction 37 { 38 // public override bool keepWaiting => this.runningCoroutineCount > 0; 39 public override bool keepWaiting => false; 40 }

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

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

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

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

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

guest

回答3

0

##多段階のコルーチン割り込みについて

何かいい手はないかと思って考えてみたものの、使用方法が複雑になってしまっていまいち満足のいく結果にはできませんでした。
動作的にも任意のコルーチンに割り込むことはできず、あらかじめ割り込めるように作っておいたコルーチンでないといけないという不完全な状態です。汎用的に使えるようにするには、もっとじっくり作りを考える必要がありそうですね...

まず、往復運動を行うオブジェクトのためのスクリプトはご質問者さんのコードをベースに下記のようにしました。

C#

1using System.Collections; 2using System.Collections.Generic; 3using UnityEngine; 4 5public class PingPongTest : MonoBehaviour 6{ 7 [SerializeField] GameObject startPoint; 8 [SerializeField] GameObject endPoint; 9 [SerializeField] float animationLength = 1.0f; 10 11 readonly float speed = 4; 12 readonly Stack<WaitForInterruption> interrupters = new Stack<WaitForInterruption>(); 13 14 void Start() 15 { 16 Vector3 startPosition = startPoint.transform.position; 17 Vector3 endPosition = endPoint.transform.position; 18 startPosition.y = 0.0f; 19 endPosition.y = 0.0f; 20 StartCoroutine(PingPong(transform, startPosition, endPosition)); 21 } 22 23 // 往復運動はコルーチンとして記述し、Start中で起動する 24 IEnumerator PingPong(Transform trf, Vector3 fromPosition, Vector3 toPosition) 25 { 26 trf.position = fromPosition; 27 while (true) 28 { 29 yield return Move(trf, toPosition); 30 yield return Turn(trf); 31 yield return Move(trf, fromPosition); 32 yield return Turn(trf); 33 } 34 } 35 36 // 外部スクリプトからこのオブジェクトに対して会話を行う場合、このメソッドを実行する 37 // 外部スクリプト中に実際の会話処理を記述し、第2引数のconversationに渡す 38 public YieldInstruction Converse(Transform withTransform, IEnumerator conversation) 39 { 40 if (conversation == null) 41 { 42 return null; 43 } 44 return Interrupt(LookAtTarget(transform, withTransform, conversation)); 45 } 46 47 // 割り込み処理はこのメソッドで始動する 48 YieldInstruction Interrupt(IEnumerator coroutine) 49 { 50 if (interrupters.Count > 0) 51 { 52 return interrupters.Peek().Interrupt(coroutine); 53 } 54 return StartCoroutine(coroutine); 55 } 56 57 // 割り込み可能なコルーチンにおいては、先頭でこのメソッドを実行することにする 58 // このメソッドで新しい割り込み階層を作るとともに、割り込まれた側を 59 // 待機させるためのYieldInstructionを返す 60 WaitForInterruption GetInterrupter() 61 { 62 WaitForInterruption interrupter = new WaitForInterruption(this); 63 interrupters.Push(interrupter); 64 return interrupter; 65 } 66 67 // 割り込み可能コルーチンの末尾では、このメソッドを実行し割り込み階層を削除することにする 68 void EndWaitInterruption() 69 { 70 interrupters.Pop(); 71 } 72 73 // 往復運動のうち片道の直線運動を担当する割り込み対応コルーチン 74 // 通常であればyield return null;とするところが、yield return interrupter;となっている 75 IEnumerator Move(Transform trf, Vector3 toPosition) 76 { 77 WaitForInterruption interrupter = GetInterrupter(); 78 while (true) 79 { 80 Vector3 currentPosition = trf.position; 81 Vector3 nextPosition = Vector3.MoveTowards(currentPosition, toPosition, speed * Time.deltaTime); 82 if (currentPosition == nextPosition) 83 { 84 break; 85 } 86 trf.LookAt(toPosition); 87 trf.position = nextPosition; 88 yield return interrupter; 89 } 90 EndWaitInterruption(); 91 } 92 93 // 往復運動のうち末端での180°転回を担当する割り込み対応コルーチン 94 // 同じくyield return null;とするところがyield return interrupter;となっている 95 IEnumerator Turn(Transform trf) 96 { 97 WaitForInterruption interrupter = GetInterrupter(); 98 Vector3 baseForward = trf.forward; 99 for (float time = 0.0f; time < animationLength; time += Time.deltaTime) 100 { 101 float t = time / animationLength; 102 Quaternion q = Quaternion.Euler(0.0f, Mathf.Lerp(0.0f, 180.0f, t), 0.0f); 103 trf.forward = q * baseForward; 104 yield return interrupter; 105 } 106 trf.forward = Quaternion.Euler(0.0f, 180.0f, 0.0f) * baseForward; 107 EndWaitInterruption(); 108 } 109 110 // 相手の方を向き、何らかの動作を行い、元の向きに戻る割り込み対応コルーチン 111 // 同じくyield return null;とするところがyield return interrupter;となっている 112 IEnumerator LookAtTarget(Transform trf, Transform target, IEnumerator action) 113 { 114 WaitForInterruption interrupter = GetInterrupter(); 115 116 // 相手の方を向き... 117 Vector3 fromDirection = Vector3.ProjectOnPlane(trf.forward, Vector3.up).normalized; 118 Vector3 toDirection = Vector3.ProjectOnPlane(target.position - trf.position, Vector3.up).normalized; 119 float angle = Vector3.SignedAngle(fromDirection, toDirection, Vector3.up); 120 for (float time = 0.0f; time < animationLength; time += Time.deltaTime) 121 { 122 float t = time / animationLength; 123 Quaternion q = Quaternion.Euler(0.0f, Mathf.Lerp(0.0f, angle, t), 0.0f); 124 trf.forward = q * fromDirection; 125 yield return interrupter; 126 } 127 trf.forward = toDirection; 128 129 // 動作を行い... 130 if (action != null) 131 { 132 yield return action; 133 } 134 135 // 元の方向へ向きなおる 136 for (float time = 0.0f; time < animationLength; time += Time.deltaTime) 137 { 138 float t = time / animationLength; 139 Quaternion q = Quaternion.Euler(0.0f, Mathf.Lerp(angle, 0.0f, t), 0.0f); 140 trf.forward = q * fromDirection; 141 yield return interrupter; 142 } 143 trf.forward = fromDirection; 144 EndWaitInterruption(); 145 } 146 147 class WaitForInterruption : CustomYieldInstruction 148 { 149 readonly MonoBehaviour driver; 150 int runningCoroutineCount; 151 152 public WaitForInterruption(MonoBehaviour coroutineDriver) 153 { 154 driver = coroutineDriver; 155 } 156 157 public override bool keepWaiting => this.runningCoroutineCount > 0; 158 159 public YieldInstruction Interrupt(IEnumerator coroutine) 160 { 161 return driver.StartCoroutine(Execute(coroutine)); 162 } 163 164 IEnumerator Execute(IEnumerator coroutine) 165 { 166 this.runningCoroutineCount++; 167 yield return coroutine; 168 this.runningCoroutineCount--; 169 } 170 } 171}

これに対して、下記のようなスクリプトをアタッチしたオブジェクトが会話を試みます。

C#

1using System.Collections; 2using UnityEngine; 3 4public class CharacterController : MonoBehaviour 5{ 6 [SerializeField] PingPongTest pingPongTest; 7 [SerializeField] KeyCode key; 8 9 IEnumerator Start() 10 { 11 while (true) 12 { 13 transform.LookAt(pingPongTest.transform); 14 if (Input.GetKeyDown(key)) 15 { 16 yield return pingPongTest.Converse(transform, Converse()); 17 } 18 yield return null; 19 } 20 } 21 22 IEnumerator Converse() 23 { 24 Say("Hello!"); 25 yield return new WaitForSeconds(3.0f); 26 Say("How are you?"); 27 yield return new WaitForSeconds(3.0f); 28 Say("Bye!"); 29 } 30 31 void Say(string text) 32 { 33 Debug.Log($"{name}: \"{text}\""); 34 } 35}

実行してみたところ、下図のように直線移動中、または180°展開中に動きを一時停止し、相手の方を向いて会話する動きをしました。

図1

また、会話モーション中にさらに会話を行おうとすると、最初の会話モーションを一時中断して新たな会話を開始し、それが終わってから最初の会話モーションを再開する動きをしました。

図2

ただし、そのような動きができるのはyield return interrupter;で待機している場合だけです。会話の実処理は会話相手のスクリプトに記述されており、そこでの待機はyield return new WaitForSeconds(3.0f);といった風に通常の待機方法ですので再開をブロックすることができず、この実処理中に割り込みを試みると下図のように動きが狂ってしまう...といういまいちな感じです。

図3

投稿2021/02/06 15:27

Bongo

総合スコア10811

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

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

0

ベストアンサー

ご提示のコードを試してみたところ、ビリヤード玉のように跳ね返るような動きをしました。おっしゃるように、X方向とZ方向の周期が異なるためでしょうね。
では、代わりに起点と終点の距離をPingPonglengthとするのはどうでしょうか?

C#

1 [SerializeField] private GameObject startPoint; 2 [SerializeField] private GameObject endPoint; 3 4 float startTime; 5 float speed = 2; 6 Vector3 dir; 7 Vector3 startPosition; 8 float distance; 9 10 void Start() 11 { 12 startTime = Time.time; 13 startPosition = startPoint.transform.position; 14 Vector3 endPosition = endPoint.transform.position; 15 dir = (endPosition - startPosition).normalized; 16 distance = Vector3.Distance(startPosition, endPosition); 17 } 18 19 void Update() 20 { 21 float t = (Time.time - startTime) * speed; 22 transform.position = startPosition + (dir * Mathf.PingPong(t, distance)); 23 24 // 進行方向は、PingPongの式の中にある「t - length」の 25 // 符号で判断できるんじゃないでしょうか? 26 transform.rotation = Quaternion.LookRotation(dir * Mathf.Sign(this.distance - Mathf.Repeat(t, distance * 2.0f))); 27 }

PingPonglengthに0以下の値を入れると狂うのは、どうやら確からしいですね。PingPongRepeatClampの式にマイナスのlengthを代入して手計算してみると確認できるかと思います。

水平の動きに制限

C#

1 void Start() 2 { 3 startTime = Time.time; 4 startPosition = startPoint.transform.position; 5 Vector3 endPosition = endPoint.transform.position; 6 7 // 下記のようにすればstartPositionを 8 // 通る水平面上を動くはずです 9 endPosition.y = startPosition.y; 10 11 // あるいは、起点・終点のy成分を0にしてしまえば 12 // 高さを無視してY=0の平面上を移動するでしょう 13 //startPosition.y = 0.0f; 14 //endPosition.y = 0.0f; 15 16 dir = (endPosition - startPosition).normalized; 17 distance = Vector3.Distance(startPosition, endPosition); 18 } 19 20 void Update() 21 { 22 float t = (Time.time - startTime) * speed; 23 Vector3 newPosition = startPosition + (dir * Mathf.PingPong(t, distance)); 24 25 // さまざまな高さの建物がある地形とのことですが、それらの高さを無視して 26 // 貫通して動くのではなく、建物の屋上に乗っているように見せたい場合は 27 // Raycastも併用して位置を修正する必要があるかと思います 28 // 「移動オブジェクトのY軸座標は変化させずに」とのことですので、 29 // そういうことをしたいのではないだろうとは思うのですが... 30 /* 31 if (Physics.Raycast(new Ray(new Vector3(newPosition.x, 100.0f, newPosition.z), Vector3.down), out var hitInfo)) 32 { 33 newPosition = hitInfo.point; 34 } 35 */ 36 37 transform.position = newPosition; 38 transform.rotation = Quaternion.LookRotation(dir * Mathf.Sign(this.distance - Mathf.Repeat(t, distance * 2.0f))); 39 }

##PingPongの一時停止

C#

1 [SerializeField] private GameObject startPoint; 2 [SerializeField] private GameObject endPoint; 3 [SerializeField] private float animationLength = 1.0f; 4 5 float startTime; 6 float pauseTime; 7 bool paused; 8 float speed = 2; 9 Vector3 dir; 10 Vector3 startPosition; 11 float distance; 12 int directionSign; 13 14 public void Pause() 15 { 16 // 一時停止フラグを立て、現在の時刻を記録する 17 paused = true; 18 pauseTime = Time.time; 19 } 20 21 public void Resume() 22 { 23 // 移動再開時に、停止していた時間をstartTimeに加算する 24 paused = false; 25 startTime += Time.time - pauseTime; 26 } 27 28 void Start() 29 { 30 startTime = Time.time; 31 startPosition = startPoint.transform.position; 32 Vector3 endPosition = endPoint.transform.position; 33 startPosition.y = 0.0f; 34 endPosition.y = 0.0f; 35 dir = (endPosition - startPosition).normalized; 36 distance = Vector3.Distance(startPosition, endPosition); 37 38 // 進行方向の符号...1で順方向、-1で逆方向 39 // スタート時点の進行方向は順方向としておく 40 directionSign = 1; 41 } 42 43 void Update() 44 { 45 if (paused) 46 { 47 return; 48 } 49 50 float t = (Time.time - startTime) * speed; 51 Vector3 newPosition = startPosition + (dir * Mathf.PingPong(t, distance)); 52 transform.position = newPosition; 53 54 int newDirectionSign = (int)Mathf.Sign(distance - Mathf.Repeat(t, distance * 2.0f)); 55 if (newDirectionSign != directionSign) 56 { 57 // もし進行方向が反転したら、一時停止して 58 // 進行方向を更新、転回コルーチンを開始する 59 Pause(); 60 directionSign = newDirectionSign; 61 StartCoroutine(Turn(transform)); 62 return; 63 } 64 65 transform.rotation = Quaternion.LookRotation(dir * newDirectionSign); 66 } 67 68 IEnumerator Turn(Transform trf) 69 { 70 Vector3 baseForward = trf.forward; 71 for (float time = 0.0f; time < animationLength; time += Time.deltaTime) 72 { 73 float t = time / animationLength; 74 Quaternion q = Quaternion.Euler(0.0f, Mathf.Lerp(0.0f, 180.0f, t), 0.0f); 75 trf.forward = q * baseForward; 76 yield return null; 77 } 78 trf.forward = Quaternion.Euler(0.0f, 180.0f, 0.0f) * baseForward; 79 80 // 転回終了後に移動を再開 81 Resume(); 82 }

##目標方向へ回転

C#

1 [SerializeField] GameObject startPoint; 2 [SerializeField] GameObject endPoint; 3 [SerializeField] float animationLength = 1.0f; 4 [SerializeField] Transform target; 5 Vector3 dir; 6 int directionSign; 7 float distance; 8 bool paused; 9 float pauseTime; 10 readonly float speed = 2; 11 Vector3 startPosition; 12 13 float startTime; 14 15 void Start() 16 { 17 startTime = Time.time; 18 startPosition = startPoint.transform.position; 19 Vector3 endPosition = endPoint.transform.position; 20 startPosition.y = 0.0f; 21 endPosition.y = 0.0f; 22 dir = (endPosition - startPosition).normalized; 23 distance = Vector3.Distance(startPosition, endPosition); 24 directionSign = 1; 25 } 26 27 void Update() 28 { 29 if (paused) 30 { 31 return; 32 } 33 34 float t = (Time.time - startTime) * speed; 35 Vector3 newPosition = startPosition + (dir * Mathf.PingPong(t, distance)); 36 transform.position = newPosition; 37 int newDirectionSign = (int)Mathf.Sign(distance - Mathf.Repeat(t, distance * 2.0f)); 38 if (newDirectionSign != directionSign) 39 { 40 Pause(); 41 directionSign = newDirectionSign; 42 StartCoroutine(Turn(transform)); 43 return; 44 } 45 46 transform.rotation = Quaternion.LookRotation(dir * newDirectionSign); 47 48 // 実験として、スペースバーで模擬的な会話を行う 49 if (!paused && Input.GetKeyDown(KeyCode.Space)) 50 { 51 StartCoroutine(TalkWith(target)); 52 } 53 } 54 55 public void Pause() 56 { 57 // 今さらながら、もし一時停止中に再度Pauseを行うと時刻が狂うため 58 // 一時停止中には時刻の記録を行わないようにしました 59 if (paused) 60 { 61 return; 62 } 63 64 paused = true; 65 pauseTime = Time.time; 66 } 67 68 public void Resume() 69 { 70 // 同じく時刻の狂いを防止するため、移動中には 71 // 開始時刻の更新を行わないようにしました 72 if (!paused) 73 { 74 return; 75 } 76 77 paused = false; 78 startTime += Time.time - pauseTime; 79 } 80 81 IEnumerator TalkWith(Transform targetTransform) 82 { 83 // 念のため目標方向をXZ平面上に投影しておく 84 Vector3 targetDir = Vector3.ProjectOnPlane(targetTransform.position - transform.position, Vector3.up).normalized; 85 Vector3 forward = transform.forward; 86 87 // 一時停止して目標方向へ回転し... 88 Pause(); 89 yield return YTurnToTarget(transform, targetDir); 90 91 // コンソールに文章を表示、少し待機する(会話しているつもり) 92 Debug.Log("Hello!"); 93 yield return new WaitForSeconds(5.0f); 94 95 // 元の方向へ回転し、一時停止を解除する 96 yield return YTurnToTarget(transform, forward); 97 Resume(); 98 } 99 100 // 引数としてターゲットへの方角を受け取ることにする 101 IEnumerator YTurnToTarget(Transform trf, Vector3 targetDir) 102 { 103 Vector3 baseForward = trf.forward; 104 105 // Y軸周りの符号付き回転角を得て... 106 float angle = Vector3.SignedAngle(baseForward, targetDir, Vector3.up); 107 for (float time = 0.0f; time < animationLength; time += Time.deltaTime) 108 { 109 float t = time / animationLength; 110 111 // Lerpの引数としてangleを使う 112 Quaternion q = Quaternion.Euler(0.0f, Mathf.Lerp(0.0f, angle, t), 0.0f); 113 trf.forward = q * baseForward; 114 yield return null; 115 } 116 117 // 補間値t=1の調整処理 118 trf.forward = Quaternion.Euler(0.0f, angle, 0.0f) * baseForward; 119 } 120 121 IEnumerator Turn(Transform trf) 122 { 123 Vector3 baseForward = trf.forward; 124 for (float time = 0.0f; time < animationLength; time += Time.deltaTime) 125 { 126 float t = time / animationLength; 127 Quaternion q = Quaternion.Euler(0.0f, Mathf.Lerp(0.0f, 180.0f, t), 0.0f); 128 trf.forward = q * baseForward; 129 yield return null; 130 } 131 132 trf.forward = Quaternion.Euler(0.0f, 180.0f, 0.0f) * baseForward; 133 Resume(); 134 }

##コルーチンの生存確認

案1

MyCoroutine内でのnullチェックは削除し、Updateでのコルーチン起動判定条件としてnullチェックを行うのはどうでしょう。

C#

1using System.Collections; 2using UnityEngine; 3 4public class TestCoroutine : MonoBehaviour 5{ 6 Coroutine RunningMyCoroutine; 7 8 void Start() 9 { 10 RunningMyCoroutine = StartCoroutine(MyCoroutine()); 11 } 12 13 void Update() 14 { 15 if ((RunningMyCoroutine == null) && Input.GetMouseButtonDown(0)) 16 { 17 RunningMyCoroutine = StartCoroutine(MyCoroutine()); 18 } 19 } 20 21 IEnumerator MyCoroutine() 22 { 23 int i = 0; 24 while (i < 5) 25 { 26 Debug.Log($"MyCoroutine:{i}"); 27 i++; 28 yield return new WaitForSeconds(2.0f); 29 } 30 RunningMyCoroutine = null; 31 } 32}

案2

yield returnを行わずにいきなり終了するコルーチンを始動した場合StartCoroutineの返り値はnullになるようなので、「返り値がnullならRunningMyCoroutineは元の値のままとする」ということにしてもよさそうです。

C#

1using System.Collections; 2using UnityEngine; 3 4public class TestCoroutine : MonoBehaviour 5{ 6 7 Coroutine RunningMyCoroutine; 8 9 void Start() 10 { 11 RunningMyCoroutine = StartCoroutine(MyCoroutine()); 12 } 13 14 void Update() 15 { 16 if (Input.GetMouseButtonDown(0)) 17 { 18 RunningMyCoroutine = StartCoroutine(MyCoroutine()) ?? RunningMyCoroutine; 19 } 20 } 21 22 IEnumerator MyCoroutine() 23 { 24 if (RunningMyCoroutine != null) yield break; 25 int i = 0; 26 while (i<5) 27 { 28 Debug.Log($"MyCoroutine:{i}"); 29 i++; 30 yield return new WaitForSeconds(2.0f); 31 } 32 RunningMyCoroutine = null; 33 } 34}

投稿2021/01/31 00:09

編集2021/02/09 21:21
Bongo

総合スコア10811

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

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

退会済みユーザー

退会済みユーザー

2021/01/31 17:17

ご回答ありがとうございます。 PingPongメソッドやRepeatメソッドのlengthに0以下の値を入れると狂うのですね、リンクのソースを拝見しました。 コードのご教示ありがとうございます。 ご提示のコードを試したところ、往復運動の挙動をしていることを確認しました。 なるほど、終点への方向ベクトルを取得しておいて、その長さをPingPongメソッドで調整すればよいというわけですね、勉強になります。 こちらのコード、transform.position.yでログを取ったところ、移動オブジェクトのy座標が変動していまして、Y軸移動も含める場合は、こちらのコードも参考にさせていただきたいと思っていますが、 質問が拙くてすみません、 今回はイメージ的には、高さの異なる建物オブジェクト等をstartPointとendPointとして登録しておいて、キャラクタが平面の地面を往復させるようなことを実装したいと思っていて、 移動オブジェクトのY軸座標は変化させずに、往復運動を実装するには、どういったコードを書けばよいですか? >進行方向は、PingPongの式の中にある「t - length」の >符号で判断できるんじゃないでしょうか? すみません、こちら理解できていなくて、単純にPingPongメソッドを使ったときでも、 「t-length」の符号で、往路と復路を判定できるということでしょうか? 質問欄に追記として記載したコードを試してみたのですが、 確かに最初の0~length未満の往路では、負の値となり、往路とわかるのですが、 それ以降はずっと正の値(2回目の往路のときt=7~8も、t-lengthは4~5と正の値)となっていて、 往路、復路はどのように判定できますか? ...と思ったのですが、コードを調べさせていただいているうちに気づいたのですが、 「length - Mathf.Repeat(t, length * 2.0f)」の符号で、往路か復路か判定できるということでしょうか? それとも、もっと簡略化したコードの符号で、往路と復路の判定ができますか? こちらも、試したコードを質問欄の追記に記載しました。 また、Quaternion.LookRotationメソッドですが、これは、第2引数の上方向の定義は、LookAt関数の第2引数と同様で、その上方向を意識しつつ、オイラー回転軸のZ軸を第1引数のベクトルに合わせた回転を取得するメソッドという認識で合っていますか?
Bongo

2021/01/31 20:35

動きを水平面上に制限するのであれば、回答に追記しましたようにY成分を書き換えてしまってはいかがでしょうか。 往路・復路の判定についてはおっしゃる通りです。PingPongメソッドは... // PingPongs the value t, so that it is never larger than length and never smaller than 0. public static float PingPong(float t, float length) { t = Repeat(t, length * 2F); return length - Mathf.Abs(t - length); } という風に、1行目でいきなりtの値をRepeatを使った値で置き換えてしまっています。回答中で申し上げた「t - length」のtは、この書き換え後のtを指しているつもりでした。言葉足らずですみません... LookRotationについてはおっしゃる通りLookAtと同じような挙動になりますね。もし第2引数を指定してやれば、オブジェクトの上方向が第2引数で与えた方向に近づくようZ軸周り回転が設定されるはずです。
退会済みユーザー

退会済みユーザー

2021/02/01 16:48

ご回答ありがとうございます。 ご教示いただいたコードでy軸方向に移動しなくなったことを確認いたしました。 開始点と終了点のオブジェクトのy座標を合わせればよいということですね。 「endPosition.y = startPosition.y;」ですが、 移動オブジェクトのy座標を現状維持にしたいという場合は、 「endPosition.y = startPosition.y = transform.position.y;」と書き換えればよいですか? (視認した限りでは、大丈夫そうに見えました。) 往復途中に障害物がある場合を考慮した方法を教えていただき、ありがとうございます。 このような処理もできるようになりたいと思っていましたが、 とても難しそうな処理なので後々勉強しようと思っていたのですが、 まさかこんな斬新な方法で実装できてしまうなんて、驚きました。 上空からレイを落として、障害物検知しているわけですね、勉強になります。 ただ、こちらのコードを実際に試したところ、 ゲーム開始直後に移動オブジェクトがものすごい勢いで上空に吹っ飛んでいきまして、 調べたところ、移動オブジェクトそのものにレイがヒットしていることがわかりました。 また、障害物が置かれていない地面そのものにレイがヒットした場合に、 「newPosition = hitInfo.point;」を実行しないほうが、より処理が軽くなりますか? それらを踏まえて、レイのif文内を以下のようなコードで組んでみたのですが、いかがでしょうか? (むしろGroundの判定をするほうが処理が重くなりますか?) if(!(hitInfo.collider.CompareTag("Player") || hitInfo.collider.CompareTag("Ground"))){ newPosition = hitInfo.point; } ...と思いましたが、このコードで上空に飛ぶ挙動はなくなったのですが、障害物を乗り越えなくて、 ログを取ってみた所、障害物付近でも常にPlayerを検知していました。 詳細は質問の追記②に記載しました。 どういった改善をすればよいですか? 記載した動画では、緑色のキューブを障害物として見立てています。 また、動画ではあまり撮影しきれませんでしたが、折り返し地点の建物(プロトタイプなので白いキューブ)にも乗り上げますが、ずっと乗っているのではなく、地面と建物をすばやく上下運動してしまうのも気になります。 「t-length」のご解説ありがとうございます。なるほど、PingPongメソッドの内部のお話だったのですね。 LookRotationメソッドのご説明ありがとうございます。 LookRotationメソッドは使ったことがなく、意図した回転にならないと思い込んでいたのですが、 よく調べたらFromToRotationメソッドの勘違いでした。 用法的には、LookAtメソッドのベクトル版とご教示いただいたので、今後は、LookRotationメソッドも意識してみようと思います。
Bongo

2021/02/01 23:14 編集

地面にレイがヒットしたケースですが、hitInfo.collider.CompareTagで判定してnewPositionへの代入を行うかどうか決める...というのは、おそらくかえって負荷が大きくなりそうに思います(実際に検証したわけではないですし、増えたとしてもわずかだろうとは思いますが)。 newPositionは単なるVector3変数ですし、hitInfo.pointはRaycastHit構造体のpointプロパティからVector3を取得するだけで、そのpointプロパティも構造体内の実際にデータを格納しているフィールドであるm_Pointから値を持ってきているだけのようでした(https://github.com/Unity-Technologies/UnityCsReference/blob/61f92bd79ae862c4465d35270f9d1d57befd1761/Modules/Physics/ScriptBindings/Dynamics.bindings.cs#L54 )。ですのでnewPosition = hitInfo.point;には大して処理コストはかからないだろうと思われます。 今回のようなケースで処理コストを削減するとしたら、地面やプレイヤーに専用のレイヤーを割り当て、Raycast時にレイヤーマスクも併用してこれらレイヤーを除外する、あるいは逆にヒットさせたい建物だけに専用のレイヤーを割り当て、レイヤーマスクはそのレイヤーだけを対象にする...というのが常套手段かと思います(https://docs.unity3d.com/ja/current/Manual/Layers.html の「選択的なレイキャスト」とかhttps://kan-kikuchi.hatenablog.com/entry/RayCast2 などがご参考になるでしょうか)。 これならそもそも地面やプレイヤーにレイがヒットしませんので、衝突情報を集めてRaycastHitを作る処理自体が省かれるだろうと思います。 なお、レイヤーマスクは自前で適切なビットを立てて構築するか、LayerMask.GetMask(https://docs.unity3d.com/ja/current/ScriptReference/LayerMask.GetMask.html )で構築することができますが、これはRaycastのたびに毎回計算する必要はないだろうと思います。ゲーム開始時にでも1回だけ求めておいて、ゲーム中ではそれを使うだけにするのが処理コスト削減には有効でしょう。 LayerMaskをインスペクター上で設定できるようにするのもいいんじゃないかと思います。LayerMask型はインスペクター上ではカメラのカリングマスクみたいなポップアップメニューで表示されますので、視覚的にもわかりやすいでしょう(https://indie-du.com/entry/2015/02/09/200000 )。 緑の建物の上に乗らない現象については、確かに緑のキューブよりも先にプレイヤーにレイがヒットしてしまえばそうなるでしょうね。先ほど申し上げたレイヤーマスクによる対象選別ならプレイヤーにレイをヒットさせなくすることができますので、この症状を回避できるかと思います。 終点で上下にピョコピョコ動いてしまう件ですが、終点の位置設定がキューブの端ギリギリだったりしませんかね?もしかするとRaycastのヒット判定に微妙な誤差が生じているのかもしれません。終点をしっかりキューブの位置に置いてもそうなってしまうでしょうか?
退会済みユーザー

退会済みユーザー

2021/02/02 15:25

ご回答ありがとうございます。 なるほど、大してコストのかからない処理だったのですね、勉強になります。 Rayでヒットするオブジェクトの選別をする場合、ヒット情報から調べるのではなく、レイヤーで設定してしまうのがよいのですね。 今までRaycastメソッドでレイヤー設定を扱ったことがなかったので、ご提示のサイト、勉強になりました。 >地面やプレイヤーに専用のレイヤーを割り当て、Raycast時にレイヤーマスクも併用してこれらレイヤーを除外する、 こちらの対象レイヤーを除外する方法に関してですが、 ビット演算でレイヤーマスクを作る方法で、ビット否定演算子(~)を使う方法という認識で合っていますか? (実際に試して除外されることを確認しましたが、私が別の方法と勘違いしていないかという意味で質問させていただいています)。 Raycastについて調べていたところ、Mathf.Infinityとfloat.PositiveInfinityが出てきて違いが気になったのですが、Mathf.Infinityの内部で、Single.PositiveInfinity(float.PositiveInfinity)が呼ばれていることがわかりました。 public const float Infinity = Single.PositiveInfinity; こちらも、newPositionのお話と同様、float.PositiveInfinityを呼んだほうが微かに速いけど、 コスト面では微々たる差なので、どちらを呼んでも構わないということですね。 余談ですが、Infinityを調べているうちに初歩的かつ重大なことに気づいたのですが、Mathfクラスのfは、float型のfなんですね。つまり、Mathfクラスの全プロパティと全メソッドの全ての引数と全ての戻り値は、float型で扱われているということなんですね、今まで気づきませんでした...。 ご教示いただいたレイヤーマスクの方法で、緑の建物の上に乗るようになりました。 終点で上下にピョコピョコ動いてしまう件に関しましても、ご教示いただいたレイヤーマスクの設定で解消しました。ありがとうございます。 すみません、実はもう1点、PingPongの移動処理で気になっていることがありまして、 難しそうなので後々勉強しようかなと思っていたことではあるんですが、 ご教示いただいた途中の建物に乗る処理が意外にも簡単だったので、質問させていただきたいと思いました。 折り返し地点で移動オブジェクトを反対方向に向かせる際、瞬時に切り替えるのではなく、 ゆっくりターンして向かせるにはどういった実装をすればよいですか? ターン自体の処理は、質問の追記③に考えたコードを記載しました(もっとスマートな方法があるかもしれませんが)。 ただ、これをPingPongの移動中の折り返し地点に挟み込む方法が思い浮かばないという状態です。 Time.timeは進み続けるので、ターンが終わった後に戻すと、ある程度進んだ距離から再開してしまい、不具合になると考えています。 また、こちらの質問(https://teratail.com/questions/134417)によると、Time.timeを止めると、 最後に呼ばれた時(Time.timeScale = 0になる1フレーム前)の値を維持するらしいですが、 Time.timeScale = 0にするというのは、他に影響が出てしまうことなので避けたく...、 となると、そもそもPingPongメソッドで実装すべきものではないのかなと思ったりしてしまうのですが、 いかがでしょうか? もし、PingPongの移動処理を途中で中断して、他の処理が挟み込めるようになると、 例えば、キャラが往復移動していて、話しかけたらピタっと止まり、会話が終了したら、 また往復運動を再開するといった応用ができたりするのかなと思いました。
Bongo

2021/02/02 19:44

レイヤー除外の手順はその通りですね。「~(1 << 地面レイヤー番号)」なら地面が除外、「~((1 << 地面レイヤー番号) | (1 << プレイヤーレイヤー番号))」なら地面とプレイヤーが除外、というわけです。 なおkanのメモ帳さんの記事にも言及がありますが、この状態だとIgnore RayCastレイヤーにもレイがヒットするようになるため要注意でしょう。Physics.DefaultRaycastLayers(https://docs.unity3d.com/ja/current/ScriptReference/Physics.DefaultRaycastLayers.html )を使って「Physics.DefaultRaycastLayers & (~((1 << 地面レイヤー番号) | (1 << プレイヤーレイヤー番号)))」としたり、Physics.IgnoreRaycastLayer(https://docs.unity3d.com/ja/current/ScriptReference/Physics.IgnoreRaycastLayer.html )を使って「~(Physics.IgnoreRaycastLayer | (1 << 地面レイヤー番号) | (1 << プレイヤーレイヤー番号))」としたりすれば地面・プレイヤー・Ignore RayCastが除外されるかと思います。 無限大についてですが、これにはお調べいただいた通り「const」が付いています。こういった定数をコード内で使った場合、それらはコンパイル時にそれらの値そのものに置き換えられるはずですので(参考: https://ufcpp.net/study/csharp/sp_const.html )、Mathf.InfinityでもSingle.PositiveInfinityでも速度差は出ないんじゃないかと思います。 PingPongの一時停止ですが、確かにTime.timeScale = 0としてしまうと、ゲームの世界全体の時間が停止するのでややこしくなりそうですね... 代替案として回答に追記しましたが、どれだけ停止していたかを求めて、その時間だけPingPongに与える時刻をずらすというのはどうでしょうか?
退会済みユーザー

退会済みユーザー

2021/02/03 17:24

ご回答ありがとうございます。 レイヤー除外についてご教示いただきありがとうございます。 除外においては、考慮しないとIgnore RayCastレイヤーにもレイがヒットするようになることを見逃していました、ありがとうございます。 constの場合は、微々たる差というよりも速度差が出ないんですね、勉強になります。 なるほど、どれだけ停止していたかを求めて、その時間だけずらすという手法があるのですね。 とても勉強になります。 すみません、最後に気になる点が3点ありまして(わからない点が1点、アイデアが湧いて2点)、 これらに関して最後に質問させていただきたいのですが、エラーやバグが発生していて、考える時間が足りず、少々お時間いただけたらと思います。 長々と質問が続いてしまい、すみません。
退会済みユーザー

退会済みユーザー

2021/02/05 15:57 編集

返信が遅くなり大変申し訳ありません。 たくさんお時間をいただいたのですが、結局、解決できず、エラーが膨大に出て、質問量も膨れ上がってしまいましたので、3点質問させていただきたいと申しましたが、1点目のみ質問させていただきたいと思います。 一応、自分がしたかったことのメモとして2点目、3点目も記載します。 ・1点目。  PingPongの往復運動中に、話しかけられたら、中断処理でそっちの方向に向かせたく、  ターゲットオブジェクトの方向に、Y軸周りのみで指定時間で向かせるためのコルーチンを  YTurnToTargetという名前で作成を試みたのですが、意図した通りに向かなく、  また不具合の原因がわからず、作成方法を教えていただけませんか?  今までBongo様にご教示いただいた内容を応用すればできるはずなのですが、力不足を痛感します。   こちら質問欄の追記④に記載しました。  ※以下はメモです。 ・2点目。  Time.timeの処理をコルーチン化(TimeCoroutine)し、中断処理もコルーチン化(DoSomethingCoroutine)して、さらに中断処理が簡単に行えるように、Suspensionというメソッドを作成してみたのですが、こういった設計はおかしくはないですか? 1つは、コルーチン化したいという意図と、もう1つはSuspensionという簡単に使えるような中断処理を意図した設計です。 ただ、Suspensionメソッドでエラーが出てしまい、作成できないという状態です。 こういった設計ができれば、コルーチンの割り込みの割り込みの割り込み...といった深い階層の処理ができるようになるのかなと思いました。 ただ、コルーチンの割り込みで検索してみても、今回作りたいような情報が見つからず、こういった設計はしないほうがよいのか迷っています。 そして実際組んでみると、UnityActionとかいろいろ試したのですが、どうも引数にコルーチンを渡して、受け取ったコルーチンを動かすという処理が うまくいかず、エラーが出てしまうのですが、そもそもそういったことはできませんか? あと、コルーチンの生存確認もコード中のコメントに書かせていただきましたが、フラグをフィールドに持たせて、コルーチン内部で切り替えるという処理以外では実装不可能ですか? (コルーチンの外のフィールドに管理させるフラグを持たせるというのが、あまり好きではなく)。 コルーチンの生存確認をしたい理由は、同じコルーチンを多重に動かしたくないためです。 こちら質問欄の追記⑤に記載しました。    ・3点目。 今回、Time.timeの割り込み処理として、どれだけ停止していたかを求めて再開時に加算するという方法を教えていただき、こちらの方法を大変参考にさせていただきたいと思っていますが、それとは別のアプローチとして、自作のTime.timeを扱えるクラスを作ってみるのはどうか、と思い浮かびました。 メリットとしましては、自作のTime.timeを利用して複数のオブジェクトを動かしたり、 Time.timeと関係のない他のゲームオブジェクトに影響しないので、気軽にそのtimeを止められて、再開時も、途中止めていた時間の計算をしなくて済むことだと思っています。 本当は、内部でtimeScale的なものも実装したかったのですが、エラーが解決できませんでした。 デメリットとしては、基本的に常に時間換算しているので、処理が重くなるかもしれないと思っています。 このように、自作のTime.timeを作成して利用するのはあまりよくないですか? それとも、1本動かすぐらいなら大丈夫、2~3本程度なら問題なさそう等、ご助言いただけたらと思います。 また、timeをstaticなものとして参照したいので、複数のTime.timeを使いたい場合は、スクリプトファイルを複製して、MyTime2クラスやMyTime3クラスのように、都度、スクリプトファイルを作成するしか方法はないのかと悩んでいます。 こちら質問欄の追記⑥に記載しました。  こちらはとりあえず、コルーチンの生存確認はフィールドで管理する方法しかわからなかったので、その方法で作成いたしました。 フィールドで管理する書き方とか、その他の処理をオブジェクト指向的にあまりスマートに書けなかったので、クラス設計的に改善案等ありましたら、ご助言いただけたら幸いです。
Bongo

2021/02/06 15:29

ターゲットへの回転については、追記しましたような感じでいかがでしょうかね? 会話相手の方へ向く動きと元の方向へ向き直る動きを両方カバーしようと思い、YTurnToTargetの第2引数はTransformの代わりにVector3を使い、目標方向はコルーチン呼び出し側で計算させることにしました。 コルーチンが生きているかどうかをCoroutineだけで判断する方法は確かに検索しても出てきませんね...おっしゃるように別途フラグを用意するか、あるいはそれと大差ない方法...始動したコルーチンをフィールドに保持しておいて、コルーチン末尾でフィールドをnullにするようにして、生存確認はフィールドがnullでないかで判断する...みたいなものしかないかもしれません。 多段階の割り込みについて検討してみましたので、多少なりともご参考になれば...と思い別回答(すみませんが字数の制限に達してしまいました)に追記しましたが、あまり実用的な状態ではありません。多段階割り込みはなかなかやっかいそうな印象でした。 自前で時間を管理するクラスを作る件については、はたしてうまくいくかは未検証ですがいいアイディアじゃないかと思います。確かに多数のコルーチンを動かせば負荷はかかるでしょうが、数個や数十個動かしただけで重大な負荷がかかりそうな部分はなさそうに感じますね。 処理中止の件ですが、コルーチンにおいては普通のメソッドでreturnでの処理中止に相当するのはyield breakでしょうかね。あるいはコルーチン内のメインの処理部分全体をifブロックで囲ってしまい、条件を満たさなければメイン処理を全部飛ばして末尾に飛ばしてしまう...という手もあるでしょう。
退会済みユーザー

退会済みユーザー

2021/02/09 16:52

ご回答ありがとうございます。 2点目、3点目に関しても教えていただき、大変ありがとうございます。 ご教示いただいたコード、じっくり読ませていただきました。 初めて見る概念やメソッド等があり、読むのに時間がかかってしまいました。 とても勉強になります。そして返信が遅くなってしまい、すみません。 またこれほどお時間いただいたにも関わらず、わからない点があり、 質問させていただけたらと思います、すみません。 ・1点目に関して。  なるほど、直接的な原因はVector3.SignedAngleで差分の角度を算出してなかったことだったのですね。  私のバグのあったコードでは、dir.yはワード座標を基準にした回転角度だったので、  それをbaseForwardを基準にdir.yまで回しても意味がなかったということですね。  また、Vector3.ProjectOnPlaneメソッドも必要ということが確認できました。  Vector3.ProjectOnPlaneメソッドで書かなかった場合は、目標方向がおかしくなることを確認いたしました。  Vector3.ProjectOnPlaneメソッドは、初めて知ったので少し調べてみたのですが、いろいろと使い所がありそうですね。 ・2点目に関して。  MoveTowardsメソッドですが、今まで終点到達判定を公式ドキュメントにも書いてある通り、  下記のような書き方をしていたのですが、  if (Vector3.Distance(transform.position, target.position) < 0.001f)  少しカッコ悪くて悩んでいたのですが、ご教示いただいたコードのように、  if (currentPosition == nextPosition)  という判定ができるのですね。キレイなコードでとても勉強になります。   カスタムコルーチンについてですが、調べてみて、  こちらのサイト(https://developers.wonderpla.net/entry/2016/08/30/190718)の説明は理解したのですが、  ご教示いただいたコードに関して、いくつか気になった点とわからない点があって質問させていただけたらと思います。  まず、keepWaitingですが、これがfalseになると、このカスタムコルーチンクラスの中断が終了になるかと  思うのですが、このkeepWaitingの判定は、どういったタイミングで行われているかご存知ですか?  毎フレーム行われている、もしくはイベント駆動型(?)なのか、などのことが気になっています。  公式リファレンス(https://docs.unity3d.com/ja/2019.4/ScriptReference/CustomYieldInstruction-keepWaiting.html)には、  そういった説明はないので、もしかしたら情報は出ていないかもしれませんが。  続いて、Moveメソッドなのですが、while文の中の  「yield return null;とするところが、yield return interrupter;としている」に関してなのですが、  interrupterの中で、yield return nullが見つかるはずと予想したのですが、特にそういった記述はなく、  このwhile文はどのように1フレーム待つ処理となっていますか?(while文は毎フレーム処理になっていると予想しています)  yield returnしているinterrupterの実体は、new WaitForInterruption(this)であることはわかっていて、  keepWaitingがfalseになるまで待ち続けることはわかっています。  しかし、このWaitForInterruptionクラスの中で、yield return nullが見つからなかったので、わからないという状態です。  また、遮るinterrupterがない場合(this.runningCoroutineCountが0以下の場合)、  yield return interrupterで1フレーム待つ処理が発生しない(?)と思ってしまっています。  また、戻り値がYieldInstructionのメソッドに関してですが、  YieldInstruction MethodA(){}  とあった場合、yield return MethodA  と書くことで、MethodAの処理が終了するまで、待つことになるという認識であっていますか?  カスタムコルーチンで検索すると、CustomYieldInstructionを継承するクラスの方法しか見つからないのですが、  実際は、カスタムコルーチンの実装には、このクラスの方法と、戻り値がYieldInstructionのメソッドの方法の2つがあるということですか?  コードの読解能力が低く、立て続けの質問となってしまいすみません。  ・3点目に関して。  自前で時間を管理するクラスについてのご助言ありがとうございます。  数個や数十個動かしただけで重大な負荷はかからなさそうとご助言いただけたので、  様子を見ながら、MyTimeクラスの実装を試みます。ありがとうございます。  また、コルーチンの生存確認につきまして、コルーチンをフィールドに保持しておいて、nullで判定する方法を  ご教示いただき、そして、コルーチンの中止はyield breakとご教示いただいたので、  コルーチンの生存確認をして、多重起動防止で動くコルーチンを実装してみたのですが、  テストしてマウスボタンを連打してみると、数字が飛び越えるログが出てしまい、  うまく動かず、修正方法に関して教えていただけませんか?  こちら追記⑦に記載しました。お手数おかけしてすみません。
Bongo

2021/02/09 21:21

CustomYieldInstructionのリファレンス(https://docs.unity3d.com/ja/current/ScriptReference/CustomYieldInstruction.html )によると... keepWaiting property is queried each frame after MonoBehaviour.Update and before MonoBehaviour.LateUpdate. とのことで、毎フレームチェックしているそうですね。つまりyield return interrupter;の継続判定も毎フレーム実施されているはずです。 keepWaitingがfalseだったとき1フレーム待機するのかどうか...という件に関してですが、keepWaiting内およびyield return interrupter;の直後でTime.frameCountをコンソールに出力しフレーム番号を比較してみたところ、どうやらkeepWaitingが前回falseで今回もfalseなら1フレーム待機、前回trueで今回falseなら待機せず同フレーム内で後続の行を実行、初回実行時にfalseの場合は1フレーム待機する...みたいな挙動をしていました。実際のところどのような実装になっているのかは、どうやらネイティブコードになっているようで追跡しきれませんでした... yield return MethodAの件についてはおっしゃる通りYieldInstruction、CustomYieldInstruction、またはご質問者さんのコメントにある記事(https://developers.wonderpla.net/entry/2016/08/30/190718 )の... 5.3以降はコルーチン内の呼び出しはStartCoroutine()が省略する事が可能に なりました。 これによりMonoBehaviourを継承していないクラスでも扱いやすくなりました。 // 5.3以前の書き方 public IEnumerator Coroutine(MonoBehaviour monoBehaviour){ yield return monoBehaviour.StartCorouitne(Coroutine2()); } // 5.3以降の書き方 public IEnumerator Coroutine(){ yield return Coroutine2() ; } で例示されているようにIEnumeratorを返すことで、それに従って待機するという挙動になるかと思います。 CustomYieldInstructionのコード(https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/CustomYieldInstruction.cs )によると、CustomYieldInstructionの正体はコルーチン用に使いやすく実装されたIEnumeratorであると言えそうです。また、最初に挙げましたCustomYieldInstructionのリファレンスの末尾でも... using System; using System.Collections; // Same WaitWhile implemented by inheriting from IEnumerator. class WaitWhile2 : IEnumerator { Func<bool> m_Predicate; public object Current { get { return null; } } public bool MoveNext() { return m_Predicate(); } public void Reset() {} public WaitWhile2(Func<bool> predicate) { m_Predicate = predicate; } } といった風に、CustomYieldInstructionを継承する代わりに直接IEnumeratorを実装したクラスを作って、さらに詳しく待機の挙動をコントロールする例が載っておりました。ですので、コルーチン待機のために返すオブジェクトは実質的にはYieldInstructionとIEnumeratorの2種類だと言っていいかもしれませんね。 コルーチン生存確認の件ですが、これについてはコルーチン始動側でnullチェックを行う必要がありそうですね。回答欄の字数にもう少し余裕がありましたので、こちらの回答の末尾に案を追記しました。
退会済みユーザー

退会済みユーザー

2021/02/10 19:27

ご回答ありがとうございます。 keepWaitingのご教示ありがとうございます。 毎フレームチェックされているということで承知しました。 Bongo様のご回答を拝見して気づいたのですが、私はコルーチンの初歩的な部分でとんでもない勘違いをしていたみたいです。 yield return interrupter;に関してですが、keepWaitingがfalseの場合は、このyield return interrupter;がスキップされる(コメントアウトしたのと同等になる)と思っていて、for文が瞬時にループし続けてしまうので、yield return null;はどこに書かれているのだろうと疑問に思っていました。 ただ、今回の回答を拝見して追記⑧のようなコードを試したのですが (ずっとコードばかり見続けて考えていただけなので、いろいろと試すべきでした、すみません)、 keepWaitingがfalseの場合、yield return interrupter;はyield return null;と同等の処理がされるように見えました。 つまり、keepWaitingがfalseの場合や、yield return new WaitUntil(()=>true)の場合や、 yield return new WaitWhile(()=>false)の場合のように、 yield returnで再開条件が満たされる場合は、yield return null;と同等の処理になるということですか? ちなみに、思い付きで、下記2行をそれぞれ試したのですが、同じ挙動になってしまい、 真偽値が真逆なのに、yield return null;のような振る舞いをする理由はわからなかったです。 yield return false; yield return true; コルーチンの生存確認のご教示ありがとうございます。 案2がとてもカッコいいコードで魅力的と思いました、ありがとうございます。 StartCoroutineの戻り値とコルーチンの中が走る順序についてですが、 ①右辺のStartCoroutine(MyCoroutine()) ?? RunningMyCoroutine;が実行される。 ②??演算子(Null合体演算子)の結果が戻り値として左辺のRunningMyCoroutineに代入される。 ③MyCoroutineの中が走り、下記if文が判定される。  if (RunningMyCoroutine != null) yield break; という認識で合っていますか? つまり、StartCoroutineメソッドが実行されると、その引数のコルーチンが実行される前に、 StartCoroutineメソッドの戻り値が左辺に格納される、という認識で合っていますか? また、Coroutineの型なのですが、ご提示のコードと同じusingを書いているのですが、 私の環境では、UnityEngineを付け加えないとエラーとなるみたいでした。 UnityEngine.Coroutine RunningMyCoroutine;
Bongo

2021/02/10 22:34

keepWaitingに関する挙動はご質問者さんがお試しの通り、あるいは前回のコメントで申し上げたように、falseの間は1フレーム待機してくれるようですね。ですのでyield return null;と同等になり、無限ループとともに使用した場合でもフリーズしてしまうことはなさそうです。 yield return false;とかyield return true;といった風にbool値そのものを返すというのは、CustomYieldInstruction...つまりIEnumeratorを返しているわけではないので、keepWaitingのように待機するかどうかをコントロールする目的では使えないでしょう。お試しの通り、このようなケースではyield return null;と同じく1フレーム待機になる様子です。 ちなみに、WaitUntil(https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/WaitUntil.cs )やWaitWhile(https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/WaitWhile.cs )の正体もCustomYieldInstructionであり、これらを返す場合は待機するかどうかをコントロールできるというわけですね。 boolに限らず、YieldInstructionやIEnumerator以外を返すと同じく1フレーム待機として扱われるかと思います。とはいえ、ボックス化(https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/types/boxing-and-unboxinghttps://ufcpp.net/study/csharp/RmBoxing.html )のためにヒープ領域を消費しますので(https://baba-s.hatenablog.com/entry/2018/11/14/124000 )、必要もないのにnull以外を返して1フレーム待機するというのは、わずかながらメモリの無駄と言えるでしょう。 null以外を返しても待機方法は変わらないものの、いろいろなオブジェクトを返すこと自体はできますので、これを利用して「Unity のコルーチンで結果を受け取る - Qiita」(https://qiita.com/chiepomme/items/2bccc5c6f5b803df8e57#3-ienumeratorcurrent-%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B )の例のようなコルーチン内からコルーチン外へ何らかのデータを持ち出すテクニックもありますね。 生存確認の際の実行順序については、②と③が逆のように見えますね。 いくらか想像も含んでいますが、おそらく順序としては... ① 右辺の「StartCoroutine(MyCoroutine()) ?? RunningMyCoroutine;」の評価が開始される。 ② 「??」の左右のオペランドのうち、左の「StartCoroutine(MyCoroutine())」(つまりnullかどうか判定したい側)の式の評価が開始される。 ③ 「MyCoroutine()」が実行される。yieldを使っているメソッドは見た目は通常のメソッドと大差ないので、ここでメソッド内に記述した実際の処理内容の実行が始まるように見えるかもしれないが、この時点ではIEnumeratorを生成して返すだけにとどまる。 ④ 処理は「StartCoroutine(MyCoroutine())」の地点まで戻り、StartCoroutineが実行される。StartCoroutineの引数は③で生成されたIEnumeratorとなる。StartCoroutineは引数のIEnumeratorに対して最初のMoveNext(https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.ienumerator.movenext )を行う。 ⑤ MyCoroutine内の実際の処理内容の実行が始まる。冒頭の「if (RunningMyCoroutine != null) yield break;」により結果が分岐し、もしすでにコルーチンが実行中でRunningMyCoroutineがnullでなければ、yield breakにより処理は終了する。nullならば後続の行の実行を続け、yield return new WaitForSeconds(2.0f);に到達したところで処理を中断する。 ⑥ 処理はStartCoroutine内部のMoveNextを行った地点に戻るが、このときMoveNextの返り値は⑤の結果が「中断(まだ続きのコードがある)」ならばtrue、「終了(yield breakされた、あるいは末尾に到達した)」ならfalseとなる。おそらくStartCoroutineはこの結果を見て返す値を決定しており、true...つまり中断なら中断中のコルーチンを表すCoroutineを返し、false...つまり終了ならnullを返す。 ⑦ 処理は「StartCoroutine(MyCoroutine()) ?? RunningMyCoroutine;」の地点まで戻る。⑥までの過程で左オペランドがCoroutineかnullかが決定したので、もしCoroutineなら式全体の値としてはそのCoroutineが、もしnullなら右オペランドのRunningMyCoroutineが選択される。結果として、MyCoroutineがyield breakせずに処理を始めることができた場合はRunningMyCoroutineに新しいCoroutineがセットされ、そうでなければそのままとなる。 といった感じではないかと思います。冗長になってしまいすみません... 型名を「UnityEngine.Coroutine」と書かないとエラーが出るとなりますと、可能性としては同名の型が別に存在しているため区別がつかない...といったケースでしょうかね? もし独自に「Coroutine」という名称のクラスなりを作成された心当たりがありましたら、それと区別するために「UnityEngine.Coroutine」と書く必要が生じるのは正常な動作ですので、お気になさることはないかと思います。
退会済みユーザー

退会済みユーザー

2021/02/11 14:37 編集

ご回答ありがとうございます。 私の理解力に自信がなく、念のため、質問させていただきたいのですが、 yieldにはいろいろな種類の待機処理があると思いますが、どのyieldの待機処理においても、 返されるものはYieldInstructionかIEnumeratorのどちらかで、 そのyieldの待機処理が無効な場合(待機条件がfalseの場合や再開条件がtrueの場合等)、 そのyieldの待機処理は、yield return null;として1フレーム待つ処理になる、 (厳密に言えば、yieldの待機処理において、内部でkeepWaitingが呼ばれている待機処理は、 前回keepWaitingがfalseで今回もfalseなら1フレーム待機、 前回trueで今回falseなら待機せず同フレーム内で後続の行を実行、 初回実行時にfalseの場合は1フレーム待機する...という細かい挙動の違いはある) という認識で合っていますか? また、yield return new WaitForSeconds(1.0f);の場合は、ループ内でも毎回待機処理として有効なので(無効になることはないので)、 1秒間待機した後に、1フレーム待機することはないという認識で合っていますか? (細かい質問ですみません)。 存確認の際の実行順序についてご教示いただき、ありがとうございます。 じっくり読ませていただきました。詳細な処理のご説明ありがとうございます。 初回のコルーチン呼び出しに関しても、下記のように??演算子(Null合体演算子)を付けても、 問題ないと解釈しました(もし間違ってたらご指摘お願いいたします)。 RunningMyCoroutine = StartCoroutine(MyCoroutine()) ?? RunningMyCoroutine; 型名を「UnityEngine.Coroutine」の件に関してですが、ご指摘の通り、 「Coroutine」という名称のクラスを作成していて、これが原因でした。ありがとうございます。
Bongo

2021/02/11 17:03

そうですね。yieldの挙動についてはちょっと試してみた限りではおっしゃる通りのようでした。 yield return new WaitForSeconds(1.0f);の場合もおそらくそれでいいかと思います。「1秒間待機した後に、1フレーム待機することはない」とおっしゃる点についてはちょっと意味が分からなかったのですが、WaitForSecondsのリファレンス(https://docs.unity3d.com/ja/current/ScriptReference/WaitForSeconds.html )によると時間測定はyield return new WaitForSeconds(1.0f);が行われたフレームの末尾で開始されるようです。また、「再開はt秒経過後の最初のフレームであり、正確にt秒経過後というわけではない」とのことですので、ぴったり1秒待機というわけにはいかないでしょう。1秒以上経過した後、「イベント関数の実行順序 - Unity マニュアル」(https://docs.unity3d.com/ja/current/Manual/ExecutionOrder.html )の図の中にある「yield WaitForSeconds」の地点で再開されるかと思います。 RunningMyCoroutine = StartCoroutine(MyCoroutine()) ?? RunningMyCoroutine;も問題なさそうに思いますね。 念のため申し上げますと、UnityEngine.Objectは==で特殊な挙動をするようになっており(https://docs.unity3d.com/ja/current/ScriptReference/Object-operator_eq.html の最後のあたりで言及)、UnityEngine.Objectを継承する各種クラスは破壊済みだと == nullがtrueになりますが、??はそれを無視して左オペランドが本当にnullかどうかで右オペランドを使用するかどうかを決めるみたいです。 今回の用法ではUnityEngine.Objectは関係ありませんのでいいのですが、もし他の場面で??やnull条件演算子(https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and- )を使う場合は、対象がUnityEngine.Objectの派生クラスであるかどうか、そうであれば「本当にオブジェクトを参照していない場合はnull、参照しているオブジェクトが破壊されているだけならばnullではない」という判断基準で問題ないか、といったことにご注意いただくのがいいかと思います。
退会済みユーザー

退会済みユーザー

2021/02/12 17:52

ご回答ありがとうございます。 yieldの挙動に関してご教示ありがとうございます。 WaitForSecondsの「~1フレーム待機しないという認識」に関しましては、 yieldの待機処理が無効になったら、yield return null;になるという点から、 1秒待つ→無効になる→1フレーム待機する(?)、待機しない、というような変な方向に考えが走り、 変な質問をしてしまいました、すみません。 なるほど、正確にその秒数待機というわけではないのですね。 また、イベント関数の実行順序のリファレンス拝見しました、ありがとうございます。 Null条件演算子というものもあるのですね、初めて知りました、勉強になります。 UnityEngine.Objectのnull比較の注意点に関して、ご教示いただきありがとうございます。 これは意識してないと重大なバグの原因になりそうですね、しっかり気を付けたいと思います。 PingPongの応用やRayやマスクや回転、多段階のコルーチン割り込みについて、たくさんのことをご教示いただき、大変ありがとうございました。長々と質問をさせていただき、すみません。 コルーチンに関しても大変勉強になりました。 また、ご教示いただいた多段階のコルーチン割り込みで、1段階のコルーチン割り込みも有用できることは承知していて、多段階のコルーチン割り込みの方法も大変参考にさせていただきますが、 1段階のみのコルーチン割り込みに絞った実装にも興味が湧きましたので、こちらじっくり考えてみたいと思います。 本当にたくさんのご教示、大変ありがとうございました。
guest

0

dir.xあるいはdir.zが負だった場合、

c#

1if (dir.x < 0) dx = -dx; 2if (dir.z < 0) dz = -dz;

があると
Updateのたびにdxdyの符号が変わってしまうと思うのですが

投稿2021/01/30 18:44

編集2021/01/30 18:51
modieu

総合スコア282

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

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

退会済みユーザー

退会済みユーザー

2021/01/31 17:18

ご指摘の通り、dir.xあるいはdir.zが負だった場合、Updateのたびにdxやdyの符号が変わっていました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

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

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

質問する

関連した質問