UnityでBoid'ovを最適化します





バッタに投げ入れられたバッタが、上の写真のように円を描いて行進し始めることを知っていますか? 上記の真実はバッタではなく、 ボイド -鳥、ミツバチ、魚、および他の動物の集団行動のモデルです。 モデルの単純さにもかかわらず、それは新しい特性を示しています。ボーイドが山に集まり、輪になって群れをなして飛び、人々を攻撃します。



これは記事の第2部であり、UnityおよびC#の最適化のさまざまなトリックに専念し、アルゴリズムのパフォーマンスを第1部から数十倍向上させます。



いくつかの変更



中断した場所を思い出させてください。 最適化なしの前の部分のBoid.cs
using UnityEngine; public class Boid : MonoBehaviour { public Vector3 velocity; private float cohesionRadius = 10; private float separationDistance = 5; private Collider[] boids; private Vector3 cohesion; private Vector3 separation; private int separationCount; private Vector3 alignment; private float maxSpeed = 15; private void Start() { InvokeRepeating("CalculateVelocity", 0, 0.1f); } void CalculateVelocity() { velocity = Vector3.zero; cohesion = Vector3.zero; separation = Vector3.zero; separationCount = 0; alignment = Vector3.zero; boids = Physics.OverlapSphere(transform.position, cohesionRadius); foreach (var boid in boids) { cohesion += boid.transform.position; alignment += boid.GetComponent<Boid>().velocity; if (boid != collider && (transform.position - boid.transform.position).magnitude < separationDistance) { separation += (transform.position - boid.transform.position) / (transform.position - boid.transform.position).magnitude; separationCount++; } } cohesion = cohesion / boids.Length; cohesion = cohesion - transform.position; cohesion = Vector3.ClampMagnitude(cohesion, maxSpeed); if (separationCount > 0) { separation = separation / separationCount; separation = Vector3.ClampMagnitude(separation, maxSpeed); } alignment = alignment / boids.Length; alignment = Vector3.ClampMagnitude(alignment, maxSpeed); velocity += cohesion + separation * 10 + alignment * 1.5f; velocity = Vector3.ClampMagnitude(velocity, maxSpeed); } void Update() { if (transform.position.magnitude > 25) { velocity += -transform.position.normalized; } transform.position += velocity * Time.deltaTime; Debug.DrawRay(transform.position, separation, Color.green); Debug.DrawRay(transform.position, cohesion, Color.magenta); Debug.DrawRay(transform.position, alignment, Color.blue); } }
      
      





さらに作業を簡素化し、コードを実際の生活で発生する可能性のあるものに近づける化粧品の変更から始めましょう。 ボイドのモデルを変更して、鳥のように見えると同時に三角形の数を減らします。 Blenderのシンプルなピラミッドで十分です。 プロジェクトフォルダーに.blendファイルをスローし、インスペクターでそれを選択し、インポート設定で過剰をオフにします。 古いプレハブをコピーし、実験用の新しいプレハブを作成します。







プレハブには方向が設定されているため、更新スクリプトに回転を追加する価値があります。 オブジェクトを回転させるための膨大な 数の オプションがありますが、 Vector3.RotateTowards使用します。これは単純であり、とにかく重要ではないからです。 まず、何もする必要がないかどうかを確認し、スムーズに回転します。



 if (velocity != Vector3.zero && transform.forward != velocity.normalized) { transform.forward = Vector3.RotateTowards(transform.forward, velocity, 10, 1); }
      
      





同時に、ボイドをステージに配置するコードを作り直します。 階層内のポイ捨ては悪い習慣なので、すべてのBoydesをTransform.parentで非表示にしましょう。



 var boid = Instantiate(boidPrefab, Random.insideUnitSphere * 25, Quaternion.identity) as Transform; boid.parent = transform;
      
      





ビジネスに取り掛かる



ありきたりから始めましょう。 このループでは、transform.position-boid.transform.positionの減算が3回実行されます。 これは悪いことです。結果を変数に入れる方が良いです。 100のボイドでは、これは重要ではないかもしれませんが、1サイクルで数千、1秒間に数回でさえ、違いはすでにあるでしょう。



 var vector = transform.position - boid.transform.position; if (boid != collider && vector.magnitude < separationDistance) { separation += vector / vector.magnitude; separationCount++; }
      
      





近くにVector3.magnitudeがあり、平方根の計算が必要です。 距離を比較するには、それをVector3.sqrMagnitudeに置き換えることができます。 同時に、加重ベクトルを計算する式の大きさを変更しますが、これは結果に大きな影響を与えません。



 if (boid != collider && vector.sqrMagnitude < separationDistance * separationDistance) { separation += vector / vector.sqrMagnitude; separationCount++; } … if (transform.position.sqrMagnitude > 25 * 25) { velocity += -transform.position.normalized; }
      
      





変換とGetComponent



このコードでは、 変換呼び出しは数十回以上発生し、多くの場合ループで発生します。 これにボイドの数を掛けると、悲しい画像が得られます。 変換へのアクセスは、実際には高価なコンポーネント検索を隠します。 これを回避するには、 Awake中に別の変数にキャッシュします。 このイベントは、起動時にゲームが開始される前に発生します。 同時に、トランスフォームの呼び出しをコライダーからスクリプトのパブリック変数への呼び出しに変更し、独自のコライダーと距離の2乗の条件と比較できます。



 public Transform tr; void Awake() { tr = transform; }
      
      





変換するすべての呼び出しをtrに置き換えます。



 foreach (var boid in boids) { var b = boid.GetComponent<Boid>(); cohesion += b.tr.position; alignment += b.velocity; if (vector.sqrMagnitude > 0 && (tr.position - b.tr.position).magnitude < separationDistance) { separation += (tr.position - b.tr.position) / (tr.position - b.tr.position).magnitude; separationCount++; } }
      
      





さらに最適化します



まあ、それはすでにはるかに優れていますが、ボーイズが非常に近づくと、FPSはまだ垂れ下がっています。 それはすべてPhysics.OverlapSphereがますます多くのコライダーをキャプチャし始め、すべてのboeで単純な列挙のほぼ同じ二次複雑性を得るからです。



インターネットよると、群れのツバメはわずか6ダースの隣人によって導かれています。 ボイドより悪いのは何ですか? 私たちは、サイクルをもう1つの条件に制限します。 2つの条件の場合、forループの方が適しています。 さらに、隣接の最大数だけでなく、最小数も制限することは理にかなっています。 近くに隣人がいない場合は、終了条件を追加します。 さらに、ベクトルの計算で分母を変更する必要があります。そうしないと、隣人が非常に混雑しているため、ボイドが出て行く機会がなくなります。



 private int maxBoids = 5; … boids = Physics.OverlapSphere(tr.position, cohesionRadius); if (boids.Length < 2) return; … for (var i = 0; i < boids.Length && i < maxBoids; i++) { var b = boids[i].GetComponent<Boid>(); cohesion += b.tr.position; alignment += b.velocity; var vector = tr.position - b.tr.position; if (vector.sqrMagnitude > 0 && vector.sqrMagnitude < separationDistance * separationDistance) { separation += vector / vector.magnitude; separationCount++; } } cohesion = cohesion / (boids.Length > maxBoids ? maxBoids : boids.Length);
      
      





ここでの主な問題は、速度ベクトルを頻繁に更新することです。 アルゴリズムを構成しやすくするために、一歩踏み込んでインスペクターに物事を整理します。 すべての重要な変数を公開しますが、 HideInInspector属性を使用してそれらのいくつかを非表示にし、いくつかの新しい変数を追加します。 tickパラメーターを追加し、タイマーによって呼び出し元に置き換えます。



 public int turnSpeed = 10; public int maxSpeed = 15; public float cohesionRadius = 7; public int maxBoids = 10; public float separationDistance = 5; public float cohesionCoefficient = 1; public float alignmentCoefficient = 4; public float separationCoefficient = 10; public float tick = 2; [HideInInspector] public Vector3 velocity; [HideInInspector] public Transform tr; … InvokeRepeating("CalculateVelocity", 0, tick);
      
      





次に、耳でフェイントを行い、リフレッシュレートを2秒に設定します。 はい、はい、あなたは正しいことを聞いていました。 同時に、要因を修正します。 これで、数百のボイドの代わりに、1000を作成できます。







最適化された、最適化された、しかし最適化されていない



新しい問題。 2秒に1回、すべてのボイドが新しいベクトルの計算を開始し、顕著なリップルが表示されます。 もちろん、その効果は興味深いものですが、鳥はその方法を知りません。 もう1つの簡単な最適化を行います-Random.valueを使用して計算を時間内に分散します。



 InvokeRepeating("CalculateVelocity", Random.value * tick, tick);
      
      





さて、シミュレーションの開始があまり奇妙に見えないように、AwakeではRandom.onUnitSphereからランダム性の要素も追加します



 velocity = Random.onUnitSphere * maxSpeed;
      
      





コードをより詳しく調べます。



 var b = boids[i].GetComponent<Boid>(); … var vector = tr.position - b.tr.position;
      
      





サイクルで一時変数を作成する場合、悪のガベージコレクターは遅かれ早かれプロセッサを使い果たします。 定期的に同じアクションを実行することがわかっている場合は、定数変数を作成できます。



 private Boid b; private Vector3 vector; private int i;
      
      





コードを最適化する代わりに、ロジックを最適化する方が良い場合があることを忘れないでください。 更新では、正規化が使用される球体の境界を越えるためのチェックがあります。



 velocity += -tr.position.normalized;
      
      





この関数は、そのような目的には正確すぎます。 厳密な単位ベクトルではなく方向のみが必要な場合は、ベクトルを単純に分割できます。



 velocity += -tr.position/25;
      
      





別の関数でボイドの回転を取り出してタイマーを開始すると、さらに数ミリ秒の計算を打ち切ることができます。



 InvokeRepeating("UpdateRotation", Random.value, 0.1f); … void UpdateRotation() { if (velocity != Vector3.zero && model.forward != velocity.normalized) { model.forward = Vector3.RotateTowards(model.forward, velocity, turnSpeed, 1); } }
      
      





物理学



コライダーの検索を考慮しない場合、物理学をまったく使用しないことに気づいたでしょう。 Boydsを別のレイヤーに移動し、 LayerMaskを使用してコライダーを探し、物理設定でBoyid間の衝突チェックをオフにすると、さらにリソースを節約できます。



 public LayerMask boidsLayer; … boids = Physics.OverlapSphere(tr.position, cohesionRadius, boidsLayer.value);
      
      





Physics Managerソルバーの反復カウントを最小にすると、FPSの束を取得できます。 さらに、 Time ManagerでFixed TimestepとMaximum Allowed Timestepを試してみることができますが、気が狂った場合、シミュレーションは混chaとし魅力がなくなります。



別のニュアンスは回転に関連しています。 モデルを回転させると、それに接続されている球状コライダーを回転させます。 高価で役に立たない。 この問題は、モデルを階層内のコライダーから分離することで解決されます。 そのため、さらに5 FPSを獲得できます。



 public Transform model; … if (velocity != Vector3.zero && model.forward != velocity.normalized) { model.forward = Vector3.RotateTowards(model.forward, velocity, turnSpeed, 1); }
      
      





おわりに



それだけです リソースの大部分は、フレーム内のオブジェクトの束を移動し、近隣を検索することにより消費されます。 前者で何かをするのは難しいですが、後者では物理学の使用をやめ、トリッキーなデータ構造に変更する必要がありますが、これは別の記事のトピックです。 コメントの賢明な行商人がボイドを加速するための選択肢を提供することを願っています。



Boid.csの最適化されたバージョン
 using UnityEngine; public class Boid : MonoBehaviour { public int turnSpeed = 10; public int maxSpeed = 15; public float cohesionRadius = 7; public int maxBoids = 10; public float separationDistance = 5; public float cohesionCoefficient = 1; public float alignmentCoefficient = 4; public float separationCoefficient = 10; public float tick = 2; public Transform model; public LayerMask boidsLayer; [HideInInspector] public Vector3 velocity; [HideInInspector] public Transform tr; private Collider[] boids; private Vector3 cohesion; private Vector3 separation; private int separationCount; private Vector3 alignment; private Boid b; private Vector3 vector; private int i; void Awake() { tr = transform; velocity = Random.onUnitSphere*maxSpeed; } private void Start() { InvokeRepeating("CalculateVelocity", Random.value * tick, tick); InvokeRepeating("UpdateRotation", Random.value, 0.1f); } void CalculateVelocity() { boids = Physics.OverlapSphere(tr.position, cohesionRadius, boidsLayer.value); if (boids.Length < 2) return; velocity = Vector3.zero; cohesion = Vector3.zero; separation = Vector3.zero; separationCount = 0; alignment = Vector3.zero; for (i = 0; i < boids.Length && i < maxBoids; i++) { b = boids[i].GetComponent<Boid>(); cohesion += b.tr.position; alignment += b.velocity; vector = tr.position - b.tr.position; if (vector.sqrMagnitude > 0 && vector.sqrMagnitude < separationDistance * separationDistance) { separation += vector / vector.sqrMagnitude; separationCount++; } } cohesion = cohesion / (boids.Length > maxBoids ? maxBoids : boids.Length); cohesion = Vector3.ClampMagnitude(cohesion - tr.position, maxSpeed); cohesion *= cohesionCoefficient; if (separationCount > 0) { separation = separation / separationCount; separation = Vector3.ClampMagnitude(separation, maxSpeed); separation *= separationCoefficient; } alignment = alignment / (boids.Length > maxBoids ? maxBoids : boids.Length); alignment = Vector3.ClampMagnitude(alignment, maxSpeed); alignment *= alignmentCoefficient; velocity = Vector3.ClampMagnitude(cohesion + separation + alignment, maxSpeed); } void UpdateRotation() { if (velocity != Vector3.zero && model.forward != velocity.normalized) { model.forward = Vector3.RotateTowards(model.forward, velocity, turnSpeed, 1); } } void Update() { if (tr.position.sqrMagnitude > 25 * 25) { velocity += -tr.position / 25; } tr.position += velocity * Time.deltaTime; } }
      
      









GitHubのソース | Unity Web Playerの所有者向けのオンラインバージョン



All Articles