現在、タワーディフェンスゲームを形にしようと頑張っています。その中で、経路に沿った敵の移動時の方向転換がどうも上手くいかず試行錯誤したので記録を残しておきます。
今回の経緯
今回の話に関連するこのゲームの前提
- マップは正方形セル(=ノード)のグリッドとして構成
- 歩行可能な”道”と不可能な”草”がある
- 経路は、ノードの List として管理される
- 敵の経路はスポーン地点からゴール地点までの”道”を通る最短経路
- 経路は上下左右の4方向のみで斜めには繋がらない
敵は経路(path)に登録されたノードを順番に辿っていきます。そこで、ノードに到達する(しきい値内に達する)度に次のノードに向かって回転コルーチンをスタートさせています。基本的にはこれでうまくいっていたのですが、経路の変更が入ったりメモリ使用量が多くなった瞬間などに変な挙動を示すことに気づきました。
この時の敵の移動に関するコードです。
void Update()
{
// path に従ってユニットを移動させる処理
if (path != null && path.Count > 0)
{
MoveAlongPath();
}
}
private void MoveAlongPath()
{
Vector3 direction = (targetPosition - transform.position).normalized;//ターゲットの方向
transform.position += direction * speed * Time.deltaTime;// 移動処理を行う
if (Vector3.Distance(transform.position, targetPosition) <= reachThreshold)
{
currentPathIndex++;
if (currentPathIndex < path.Count)
{
targetPosition = path[currentPathIndex].worldPosition;
Vector3 newDirection = (targetPosition - transform.position).normalized;
float targetAngle = Mathf.Atan2(newDirection.y, newDirection.x) * Mathf.Rad2Deg + 90f;
Quaternion targetRotation = Quaternion.Euler(0, 0, targetAngle);
// 回転コルーチンを開始
StartCoroutine(RotateTowardsTarget(targetRotation));
}
else
{
//ゴール到達時
onEnemyReachedGoal?.Invoke();
gameObject.SetActive(false);
}
}
}
private IEnumerator RotateTowardsTarget(Quaternion targetRotation)
{
while (Quaternion.Angle(imageObject.transform.rotation, targetRotation) > 0.1f)
{
// 回転速度に基づき、徐々に目標の回転へと向かわせる
imageObject.transform.rotation = Quaternion.RotateTowards(imageObject.transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
yield return null;
}
}
原因と解決策
題名にもあるようにコルーチンを止めずに再利用してたことが原因でした。
既存の回転コルーチンがあれば停止してから新しい回転コルーチンを始めるようにしたら問題は解決しました。
private Coroutine rotationCoroutine; // 回転コルーチンの保持
void Update()
{
// path に従ってユニットを移動させる処理
if (path != null && path.Count > 0)
{
MoveAlongPath();
}
}
private void MoveAlongPath()
{
Vector3 direction = (targetPosition - transform.position).normalized; // ターゲットの方向
transform.position += direction * speed * Time.deltaTime; // 移動処理を行う
if (Vector3.Distance(transform.position, targetPosition) <= reachThreshold)
{
currentPathIndex++;
if (currentPathIndex < path.Count)
{
targetPosition = path[currentPathIndex].worldPosition;
Vector3 newDirection = (targetPosition - transform.position).normalized;
float targetAngle = Mathf.Atan2(newDirection.y, newDirection.x) * Mathf.Rad2Deg + 90f;
Quaternion targetRotation = Quaternion.Euler(0, 0, targetAngle);
// 既存のコルーチンがあれば停止してから、新しいコルーチンを開始
if (rotationCoroutine != null)
{
StopCoroutine(rotationCoroutine);
}
rotationCoroutine = StartCoroutine(RotateTowardsTarget(targetRotation));
}
else
{
// ゴール到達時
onEnemyReachedGoal?.Invoke();
gameObject.SetActive(false);
}
}
}
private IEnumerator RotateTowardsTarget(Quaternion targetRotation)
{
while (Quaternion.Angle(imageObject.transform.rotation, targetRotation) > 0.1f)
{
// 回転速度に基づき、徐々に目標の回転へと向かわせる
imageObject.transform.rotation = Quaternion.RotateTowards(imageObject.transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
yield return null;
}
}
別の解決策
色々試行錯誤する中で見つけたコルーチンを使わない方法も残しておきます。(実は前述の原因と解決策に気づいたのは後になってからで、こちらが最適解だと途中まで思ってました。)
Update()内でコルーチンを使わずに1フレーム毎に回転処理を行うものです。回転の方向を次のノードで決めるのではなく1フレーム前の自身の位置と現在地の関係で決めています。ただ、やはりコルーチンを用いるものよりも急に敵の方向が変わり、カクカクとしてしまいました。
private Vector3 currentPos = new Vector3(0f,0f,0f);
void Update()
{
// path に従ってユニットを移動させる処理
if (path != null && path.Count > 0)
{
MoveAlongPath();
}
// 回転処理
Vector3 diff = transform.position - currentPos; // どの方向に進んでいるかがわかるように、初期位置と現在地の座標差分を取得
if (diff.magnitude > 0.01f) // ベクトルの長さが0.01fより大きい場合に向きを変える処理を入れる
{
float targetAngle = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg + 90; // 目標角度を計算
Quaternion targetRotation = Quaternion.Euler(0f, 0f, targetAngle); // Z軸周りの回転にのみ適用する
imageObject.transform.rotation = Quaternion.RotateTowards(imageObject.transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
}
currentPos = transform.position; // プレイヤーの位置を更新
}
private void MoveAlongPath()
{
Vector3 direction = (targetPosition - transform.position).normalized;//ターゲットの方向
// 移動処理を行う
transform.position += direction * speed * Time.deltaTime;
if (Vector3.Distance(transform.position, targetPosition) <= reachThreshold)
{
currentPathIndex++;
if (currentPathIndex < path.Count)
{
targetPosition = path[currentPathIndex].worldPosition;
}
else
{
//ゴール到達時
onEnemyReachedGoal?.Invoke();
gameObject.SetActive(false);
}
}
}
滑らかに曲がるには(しきい値の大切さ)
試行錯誤をする中で、重大な気づきがありました。それは、ノード到達判定のしきい値を大きくすることで曲がり角での挙動が滑らかになるというものです。
今回のゲームではノードの一辺の長さが1.0になるように作っています。初め、しきい値を 0.1f に設定していました。しかしこれだとほぼ直角に曲がることになります。しきい値 0.1f ということは、曲がり角のノードの中心から 0.1 以内の距離に近づかないと次のノードに向かわないということだからです。コルーチンを用いて滑らかに回転させても、ほぼ直角に曲がりながら進んでいくので違和感があります。そこで、しきい値を大きくすると、曲がり角において早い段階で角っこのノードに達したと判定されるので90度より鈍角に曲がって次のノードに向かってくれるのです。
例として、しきい値 0.1f , 1.0f のときの様子を図にしてみました。
赤い点線:表示されるルート 青い点:ノードの中心 オレンジの矢印:敵の実際の移動経路

いくつか制作の参考にしているタワーディフェンスのゲームでは、曲がり角で端まで行ききらずに敵が滑らかに曲がっていき、どうしたらそういった挙動をさせられるのか随分悩みましたが、これに気づいて理想通りの挙動を示したときには軽く感動してしまいました。
修正後の様子
コルーチンの問題としきい値に関して修正した後の様子です。
挙動がバグることもなくなり、曲がり角では斜めに滑らかに曲がるようになりました。理想通りです!
最後に
今回は結果的にコルーチンを使うときの注意点と、しきい値による挙動の変化を学べました。どちらも今後とも使っていく知識だと思うので、得られたものは大きい気がします。
コメント