teratail header banner
teratail header banner
質問するログイン新規登録

回答編集履歴

1

実装例を追記

2021/06/27 00:13

投稿

Bongo
Bongo

スコア10816

answer CHANGED
@@ -1,68 +1,68 @@
1
1
  ご提示のスクリプト以外にもご質問者さんオリジナルのスクリプトがいくつかあるようですので、私の方での動作確認はちょっと難しく手探りになってしまいすみませんが、まず`Moving_from_PositionA_to_PositionB`の`Start_MovementAction`周辺を下記のようにしておいて...
2
2
 
3
3
  ```lang-csharp
4
- //始動したコルーチンを覚えておくためのフィールドを追加し...
4
+ //始動したコルーチンを覚えておくためのフィールドを追加し...
5
- private Coroutine movementAction;
5
+ private Coroutine movementAction;
6
6
 
7
- public void Start_MovementAction()
7
+ public void Start_MovementAction()
8
- {
8
+ {
9
- //コルーチン始動時にそれを保存
9
+ //コルーチン始動時にそれを保存
10
- movementAction = StartCoroutine(Move());
10
+ movementAction = StartCoroutine(Move());
11
- }
11
+ }
12
12
 
13
- //そして覚えておいたコルーチンを停止するためのメソッドを用意
13
+ //そして覚えておいたコルーチンを停止するためのメソッドを用意
14
- public void Stop_MovementAction()
14
+ public void Stop_MovementAction()
15
- {
15
+ {
16
- if (movementAction != null)
16
+ if (movementAction != null)
17
- {
17
+ {
18
- StopCoroutine(movementAction);
18
+ StopCoroutine(movementAction);
19
- }
19
+ }
20
- }
20
+ }
21
21
  ```
22
22
 
23
23
  そして`Berserkermode`側の`Update`周辺は...
24
24
 
25
25
  ```lang-csharp
26
- //前回のUpdateで調べたターゲットの位置を保存するためのフィールドを追加
26
+ //前回のUpdateで調べたターゲットの位置を保存するためのフィールドを追加
27
- private Vector2Int latestTargetPos;
27
+ private Vector2Int latestTargetPos;
28
28
 
29
- private void Update()
29
+ private void Update()
30
- {
30
+ {
31
- if(get_Distance)
31
+ if(get_Distance)
32
- {
32
+ {
33
- float distanse = Vector2.Distance(gameObject.transform.position, selectedUnit.transform.position);
33
+ float distanse = Vector2.Distance(gameObject.transform.position, selectedUnit.transform.position);
34
- if (distanse <= 3)
34
+ if (distanse <= 3)
35
- {
35
+ {
36
- unit_RandomWalk_SelectedUnit.stoped_RandomWalk = true;
36
+ unit_RandomWalk_SelectedUnit.stoped_RandomWalk = true;
37
- int x = Mathf.RoundToInt(unit_RandomWalk_SelectedUnit.transform.position.x);
37
+ int x = Mathf.RoundToInt(unit_RandomWalk_SelectedUnit.transform.position.x);
38
- int y = Mathf.RoundToInt(unit_RandomWalk_SelectedUnit.transform.position.y);
38
+ int y = Mathf.RoundToInt(unit_RandomWalk_SelectedUnit.transform.position.y);
39
- unit_RandomWalk_SelectedUnit.transform.position = new Vector3(x, y, 0);
39
+ unit_RandomWalk_SelectedUnit.transform.position = new Vector3(x, y, 0);
40
- }
40
+ }
41
- }
41
+ }
42
-
42
+
43
- //対象のユニットが存在していた座標に到達したら、ではなく...
43
+ //対象のユニットが存在していた座標に到達したら、ではなく...
44
- /*
44
+ /*
45
- if (switched_Berserkermode && gameObject.transform.position == (Vector3)moving_From_PositionA_To_PositionB.moveList[moving_From_PositionA_To_PositionB.moveList.Count - 1])
45
+ if (switched_Berserkermode && gameObject.transform.position == (Vector3)moving_From_PositionA_To_PositionB.moveList[moving_From_PositionA_To_PositionB.moveList.Count - 1])
46
- {
46
+ {
47
- Go_To_TargetUnit();
47
+ Go_To_TargetUnit();
48
- }
48
+ }
49
- */
49
+ */
50
50
 
51
- if (switched_Berserkermode)
51
+ if (switched_Berserkermode)
52
- {
52
+ {
53
- //現在のターゲットの位置を調べ...
53
+ //現在のターゲットの位置を調べ...
54
- Vector2Int targetPos = new Vector2Int((int)selectedUnit.transform.position.x, (int)selectedUnit.transform.position.y);
54
+ Vector2Int targetPos = new Vector2Int((int)selectedUnit.transform.position.x, (int)selectedUnit.transform.position.y);
55
55
 
56
- //敵の位置に変化があったら...
56
+ //敵の位置に変化があったら...
57
- if (targetPos != latestTargetPos)
57
+ if (targetPos != latestTargetPos)
58
- {
58
+ {
59
- //現在動作中の移動コルーチンは中止し、目的地への経路を再計算
59
+ //現在動作中の移動コルーチンは中止し、目的地への経路を再計算
60
- latestTargetPos = targetPos;
60
+ latestTargetPos = targetPos;
61
- moving_From_PositionA_To_PositionB.Stop_MovementAction();
61
+ moving_From_PositionA_To_PositionB.Stop_MovementAction();
62
- Go_To_TargetUnit();
62
+ Go_To_TargetUnit();
63
- }
63
+ }
64
- }
64
+ }
65
- }
65
+ }
66
66
  ```
67
67
 
68
68
  とするのはどうでしょうか?
@@ -70,4 +70,276 @@
70
70
  すみませんが上記のコードは実験が不十分ですので、そのままではうまくいくかどうか不確かです。意図としては要するに、ターゲットが動くたびに経路を再計算する必要があるんじゃないか...ということでして、ご質問者さんのスタイルに合わせて自由に書き換えてしまってください。
71
71
 
72
72
  何度も再計算するのは重そうに見えるかもしれませんが、経路がよっぽど長くなる...たとえば複雑な迷路状のマップで、ターゲットにたどり着くにはかなりの回り道をしなければいけない状況が頻発するとかでなければ、案外問題ないんじゃないかという気がします。
73
- これだと計算量が多すぎるようでしたら、「[Unity A*(A-Star)アルゴリズム 挙動がおかしくなる](https://teratail.com/questions/283731)」のコメントで申し上げたような、非同期化して他の処理へのインパクトを抑制するみたいな対策が必要かもしれませんね。
73
+ これだと計算量が多すぎるようでしたら、「[Unity A*(A-Star)アルゴリズム 挙動がおかしくなる](https://teratail.com/questions/283731)」のコメントで申し上げたような、非同期化して他の処理へのインパクトを抑制するみたいな対策が必要かもしれませんね。
74
+
75
+ # 私の場合の実装例
76
+
77
+ まず、ご質問者さんの`Unit_RandomWalk`の代用品として下記のようなスクリプトを用意しました。これを各ユニットにアタッチして徘徊させます。
78
+
79
+ ```lang-csharp
80
+ using System.Collections;
81
+ using System.Collections.Generic;
82
+ using System.Linq;
83
+ using UnityEngine;
84
+
85
+ public class RandomWalk : MonoBehaviour
86
+ {
87
+ private static readonly Vector2[] Directions = {Vector2.up, Vector2.right, Vector2.down, Vector2.left};
88
+
89
+ [SerializeField][Min(0.0f)] private float idleDuration = 1.0f;
90
+ [SerializeField][Min(0.0f)] private float walkDuration = 0.5f;
91
+
92
+ private Coroutine randomWalkCoroutine;
93
+
94
+ private void Start()
95
+ {
96
+ Physics2D.queriesStartInColliders = false;
97
+ StartRandomWalk();
98
+ }
99
+
100
+ public void StartRandomWalk()
101
+ {
102
+ StopRandomWalk();
103
+ randomWalkCoroutine = StartCoroutine(RandomWalkLogic());
104
+ }
105
+
106
+ public void StopRandomWalk()
107
+ {
108
+ if (randomWalkCoroutine == null)
109
+ {
110
+ return;
111
+ }
112
+
113
+ StopCoroutine(randomWalkCoroutine);
114
+ randomWalkCoroutine = null;
115
+ }
116
+
117
+ private IEnumerator RandomWalkLogic()
118
+ {
119
+ WaitForSeconds wait = new WaitForSeconds(idleDuration);
120
+ List<Vector2> randomizedDirections = new List<Vector2>(4);
121
+ while (true)
122
+ {
123
+ yield return wait;
124
+
125
+ Vector2 currentPosition = this.transform.position;
126
+ randomizedDirections.Clear();
127
+ randomizedDirections.AddRange(Directions.Where(d => !Physics2D.Raycast(currentPosition, d, 1.0f)));
128
+ if (randomizedDirections.Count > 0)
129
+ {
130
+ Vector2 randomDirection = randomizedDirections[Random.Range(0, randomizedDirections.Count)];
131
+ Vector3 velocity = randomDirection / this.walkDuration;
132
+ float t = 0.0f;
133
+ while (true)
134
+ {
135
+ transform.Translate(velocity * Time.deltaTime);
136
+ t += Time.deltaTime;
137
+ if (t >= walkDuration)
138
+ {
139
+ break;
140
+ }
141
+ yield return null;
142
+ }
143
+ transform.position = currentPosition + randomDirection;
144
+ }
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ そして`Moving_from_PositionA_to_PositionB`ですが、後述しますが`Berserkermode`側で移動動作を行わせようと思った都合から、下記のように変更を加えました。思考の整理上(そして字数の節約上)すみませんが大部分を削除してしまいましたので、必要な部分までなくなってしまっている可能性があります。あくまでも参考ということでご容赦ください。
151
+
152
+ ```lang-csharp
153
+ using System.Collections.ObjectModel;
154
+ using UnityEngine;
155
+
156
+ namespace UtilityModule
157
+ {
158
+ public class Moving_from_PositionA_to_PositionB : MonoBehaviour
159
+ {
160
+ private readonly int map_Height = 250;
161
+ private readonly int map_Width = 250;
162
+ private ChessboardPathFinder pathFinder;
163
+
164
+ public ReadOnlyCollection<Vector2Int> Path => pathFinder.Path;
165
+
166
+ private void Awake()
167
+ {
168
+ pathFinder = new ChessboardPathFinder(map_Width, map_Height);
169
+ }
170
+
171
+ public bool Find_Path(Vector2Int startPosition, Vector2Int goalPosition)
172
+ {
173
+ bool pathFound = pathFinder.FindPath(startPosition, goalPosition);
174
+ return pathFound;
175
+ }
176
+ }
177
+ }
178
+ ```
179
+
180
+ `Berserkermode`は下記のようにしました。これも同様にちょっと作りが変わってしまいましたので、参考として割り切っていただけるとありがたいです。
181
+
182
+ ```lang-csharp
183
+ using System.Collections;
184
+ using System.Linq;
185
+ using UnityEngine;
186
+ using UtilityModule;
187
+
188
+ namespace Module_of_Berserkermode
189
+ {
190
+ public class Berserkermode : MonoBehaviour
191
+ {
192
+ [SerializeField] private Material berserkerModeMaterial;
193
+ [SerializeField] private Material nonBerserkerModeMaterial;
194
+ [SerializeField][Min(0.0f)] private float walkDuration = 0.25f;
195
+
196
+ private Moving_from_PositionA_to_PositionB moving_From_PositionA_To_PositionB;
197
+ private Coroutine berserkerModeCoroutine;
198
+ private bool needsStopBerserkerMode;
199
+ private RandomWalk randomWalk;
200
+ private new Renderer renderer;
201
+
202
+ private bool IsBerserkerMode => berserkerModeCoroutine != null;
203
+
204
+ private void Awake()
205
+ {
206
+ moving_From_PositionA_To_PositionB = gameObject.GetComponent<Moving_from_PositionA_to_PositionB>();
207
+ randomWalk = GetComponent<RandomWalk>();
208
+ renderer = GetComponent<Renderer>();
209
+ }
210
+
211
+ private void Update()
212
+ {
213
+ // 実験目的ということで単純化し、不満値は設けずに
214
+ // スペースキーでバーサーカーモードを始動するようにした
215
+ if (Input.GetKeyDown(KeyCode.Space))
216
+ {
217
+ if (IsBerserkerMode)
218
+ {
219
+ StopBerserkerMode();
220
+ randomWalk.StartRandomWalk();
221
+ }
222
+ else
223
+ {
224
+ StartBerserkerMode();
225
+ }
226
+ }
227
+ }
228
+
229
+ private void StartBerserkerMode()
230
+ {
231
+ StopBerserkerMode();
232
+
233
+ // 視覚的にわかりやすくするため、バーサーカーモード用のマテリアル(赤色)に切り替える
234
+ renderer.material = berserkerModeMaterial;
235
+
236
+ // ランダム移動を停止、バーサーカーモードコルーチンを始動する
237
+ randomWalk.StopRandomWalk();
238
+ berserkerModeCoroutine = StartCoroutine(BerserkerModeLogic());
239
+ }
240
+
241
+ private void StopBerserkerMode()
242
+ {
243
+ if (berserkerModeCoroutine == null)
244
+ {
245
+ return;
246
+ }
247
+
248
+ // ユニットがタイル間を移動中の可能性があるので、次のタイルに到達するまでは待つことにする
249
+ StartCoroutine(WaitForStopBerserkerMode());
250
+ }
251
+
252
+ private IEnumerator WaitForStopBerserkerMode()
253
+ {
254
+ if (!IsBerserkerMode)
255
+ {
256
+ yield break;
257
+ }
258
+ needsStopBerserkerMode = true;
259
+ yield return berserkerModeCoroutine;
260
+ berserkerModeCoroutine = null;
261
+ needsStopBerserkerMode = false;
262
+
263
+ // 視覚的にわかりやすくするため、徘徊モード用のマテリアル(緑色)に切り替える
264
+ renderer.material = nonBerserkerModeMaterial;
265
+
266
+ // ランダム移動を再開
267
+ randomWalk.StartRandomWalk();
268
+ }
269
+
270
+ private IEnumerator BerserkerModeLogic()
271
+ {
272
+ while (true)
273
+ {
274
+ // まず、一番近いユニットをターゲットとして選択する
275
+ Transform targetTransform = GameObject.FindGameObjectsWithTag("Target")
276
+ .Select(target => (target.transform, Vector2.Distance(target.transform.position, transform.position)))
277
+ .OrderBy(pair => pair.Item2)
278
+ .Select(pair => pair.transform)
279
+ .FirstOrDefault();
280
+
281
+ // ターゲットの上下左右4タイルのうち、一番近いタイルを目的地として採用する
282
+ Vector2Int cellPosition = Vector2Int.RoundToInt(transform.position);
283
+ Vector2Int targetCellPosition = Vector2Int.RoundToInt(targetTransform.position);
284
+ Vector2Int destinationCellPosition = GetNearestNeighborCellPosition(cellPosition, targetCellPosition);
285
+
286
+ // パス探索を行う
287
+ // パスが見つからなかった場合はバーサーカーモードを終了する
288
+ if (!moving_From_PositionA_To_PositionB.Find_Path(cellPosition, destinationCellPosition) || (moving_From_PositionA_To_PositionB.Path.Count < 2))
289
+ {
290
+ break;
291
+ }
292
+
293
+ // 探索された経路を確認する目的で、シーンビュー上に黄色い線を引く
294
+ for (int i = 1; i < moving_From_PositionA_To_PositionB.Path.Count; i++)
295
+ {
296
+ Debug.DrawLine(
297
+ (Vector2)moving_From_PositionA_To_PositionB.Path[i],
298
+ (Vector2)moving_From_PositionA_To_PositionB.Path[i - 1],
299
+ Color.yellow);
300
+ }
301
+
302
+ // 経路上の次のタイルに向かって移動する
303
+ Vector3 nextTilePosition = (Vector3)(Vector2)moving_From_PositionA_To_PositionB.Path[1];
304
+ Vector3 velocity = (nextTilePosition - transform.position) / walkDuration;
305
+ float t = 0.0f;
306
+ while (true)
307
+ {
308
+ transform.Translate(velocity * Time.deltaTime);
309
+ t += Time.deltaTime;
310
+ if (t >= walkDuration)
311
+ {
312
+ break;
313
+ }
314
+ yield return null;
315
+ }
316
+ transform.position = nextTilePosition;
317
+
318
+ // バーサーカーモード終了要求が来ていたらループを終える
319
+ if (needsStopBerserkerMode)
320
+ {
321
+ break;
322
+ }
323
+ }
324
+ StopBerserkerMode();
325
+ }
326
+
327
+ private static Vector2Int GetNearestNeighborCellPosition(Vector2Int origin, Vector2Int target)
328
+ {
329
+ int deltaX = Mathf.Clamp(origin.x - target.x, -1, 1);
330
+ int deltaY = Mathf.Abs(deltaX) > 0 ? 0 : Mathf.Clamp(origin.y - target.y, -1, 1);
331
+ return new Vector2Int(target.x + deltaX, target.y + deltaY);
332
+ }
333
+ }
334
+ }
335
+ ```
336
+
337
+ 動きは下図のようになりました。
338
+
339
+ ![図1](dfa5c77c172ebf32668fa202de5b0a89.gif)
340
+
341
+ くの字の経路になってしまい、見苦しくてすみません。一応あれでもチェス盤空間においては最短経路の一つのはずですが、遠回りしているように感じます。「[Unity A*(A-Star)アルゴリズム 挙動がおかしくなる](https://teratail.com/questions/283731)」では、経路の評価に使う情報としては浮動小数点数の計算が必要なユークリッド距離よりも、チェス盤ならチェビシェフ距離を使って経路を評価した方がいいんじゃないかと思っていたのですが、こういった見苦しさにつながる可能性は失念していました。
342
+
343
+ ユークリッド距離を使うと縦横の経路の方が斜めの経路よりも高評価になりますので、下図のように見苦しさが低減するかと思います。こちらの方がお好みでしたら`ChessboardPathFinder`を適宜書き換えてしまってください。
344
+
345
+ ![図2](af6b81cef90c75200a118d76669e069c.gif)