自然な動きの模倣:ステアリング動作-2

記事の最初の部分はこちらです。









パート6.衝突の回避



適切なNPCナビゲーションには、多くの場合、障害物を回避する機能が必要です。 このパートでは、キャラクターが周囲の障害物を安全にかわすことを可能にするステアリング動作の衝突回避について説明します。






はじめに



衝突を回避する基本的な考え方は、障害物が動きを妨げるほど接近するたびに、障害物を回避するための制御力を生成することです。 環境内に複数の障害物がある場合でも、この動作はそれらの1つを同時に使用して回避力を計算します。



キャラクターの前にある障害物のみが分析されます。 最も脅威をもたらすので、最も近いものが評価のために選択されます。 その結果、キャラクターは、エリア内のすべての障害物を安全に、ためらうことなく、次々と移動する能力を持ちます。









キャラクターの前にある障害物が分析され、最も近い(最も危険な)障害物が選択されます。



衝突回避動作は、経路探索アルゴリズムではありません。 キャラクターを環境内で移動させ、障害物を避け、徐々にブロックを通り抜ける方法を見つけますが、たとえば、LまたはTの形の障害物がある場合、うまく機能しません。



ヒント:この衝突回避動作はFleeに似ているように見えるかもしれませんが、重要な違いがあります。 壁に沿って移動するキャラクターは、そのパスをブロックするときにのみ回避し、逃げる行動は常にキャラクターを壁から押しのけます。






楽しみにして



環境内の障害を回避するために必要な最初のステップは、その認識です。 キャラクターを心配する必要がある唯一の障害は、彼の前にあり、現在のルートをブロックする障害です。



前の記事で説明したように、キャラクターの移動方向は速度ベクトルを表します。 これを使用して、 ahead



という新しいベクトルを作成します。これは、速度ベクトルのコピーですが、長さは異なります。









ahead



ベクトルは、キャラクターの視線です。




このベクトルは次のように計算されます。



 ahead = position + normalize(velocity) * MAX_SEE_AHEAD
      
      





ahead



ベクトルの長さ( MAX_SEE_AHEAD



を使用して調整可能)は、キャラクターが「見ることができる」距離を決定します。



MAX_SEE_AHEAD



ば多いMAX_SEE_AHEAD



、キャラクターは遠く離れていても脅威として認識されるため、障害物を回避するのが早くなります。









前方の長さが長いほど、キャラクターは障害物を回避するための措置を早めます。






衝突チェック



衝突をチェックするには、各障害物(またはそれを記述する長方形)を幾何学的な形で記述する必要があります。 最良の結果は、球(2次元-円)を使用することで得られるため、環境内の各障害物はこのように記述する必要があります。



1つの解決策は、セグメントと球体の交差点の衝突をチェックすることです。セグメントはahead



のベクトルであり、球体は障害物です。 このアプローチは機能しますが、私はその単純化を使用します。この単純化は理解しやすく、同時に同様の結果をもたらします(時にはさらに良くなります)。



ahead



ベクトルは、別の半分の長さのベクトルを作成するために使用されます。









同じ方向、半分の長さ。



先のベクトルahead2



ahead



とまったく同じように計算されますが、その長さは半分になります。



 ahead = position + normalize(velocity) * MAX_SEE_AHEAD ahead2 = position + normalize(velocity) * MAX_SEE_AHEAD * 0.5
      
      





衝突チェックを実行して、これら2つのベクトルのいずれかが障害物の球内にあるかどうかをテストします。 これは、ベクトルの端と球の中心との間の距離を比較することで簡単に行えます。



距離が球の半径以下の場合、ベクトルは球の内側にあり、衝突が検出されます。









d <rの場合、前方のベクトルは障害物と交差します。 より明確にするために、ahead2のベクトルは削除されます。



前方のこれら2つのベクトルのいずれかが障害物の球内にある場合、この障害物はパスをブロックします。 2点間のユークリッド距離の定義を使用できます。



 private function distance(a :Object, b :Object) :Number { return Math.sqrt((ax - bx) * (ax - bx) + (ay - by) * (ay - by)); } private function lineIntersectsCircle(ahead :Vector3D, ahead2 :Vector3D, obstacle :Circle) :Boolean { //  "center"  -  Vector3D. return distance(obstacle.center, ahead) <= obstacle.radius || distance(obstacle.center, ahead2) <= obstacle.radius; }
      
      





キャラクターのパスがいくつかの障害物をブロックしている場合、最も近い(最も危険な)障害物が計算のために選択されます。









計算のために、最も近い障害物(最も危険なもの)が選択されます。






回避計算



回避の力は、キャラクターを障害物から遠ざけ、球体を回避できるようにします。 これは、球の中心(位置ベクトル)とahead



ベクトルを使用して形成されたベクトルを使用して実装できます。 この回避力は次のように計算します。



 avoidance_force = ahead - obstacle_center avoidance_force = normalize(avoidance_force) * MAX_AVOID_FORCE
      
      





avoidance_force



計算後、 MAX_AVOID_FORCE



、つまりavoidance_force



の長さを決定する値によって正規化およびスケーリングされます。 MAX_AVOID_FORCE



多いMAX_AVOID_FORCE



、回避の力が強くなり、キャラクターが障害物から遠ざかります。









回避力の計算。 オレンジ色の破線は、障害物を避けるためにキャラクターがたどるパスを示しています。



ヒント:エンティティ位置はベクトルとして記述できるため、他のベクトルや力とともに計算に使用できます。






障害物回避



回避力を返すcollisionAvoidance()



メソッドの完成した実装は、次のようになります。



 private function collisionAvoidance() :Vector3D { ahead = ...; //   ahead ahead2 = ...; //   ahead2 var mostThreatening :Obstacle = findMostThreateningObstacle(); var avoidance :Vector3D = new Vector3D(0, 0, 0); if (mostThreatening != null) { avoidance.x = ahead.x - mostThreatening.center.x; avoidance.y = ahead.y - mostThreatening.center.y; avoidance.normalize(); avoidance.scaleBy(MAX_AVOID_FORCE); } else { avoidance.scaleBy(0); //    } return avoidance; } private function findMostThreateningObstacle() :Obstacle { var mostThreatening :Obstacle = null; for (var i:int = 0; i < Game.instance.obstacles.length; i++) { var obstacle :Obstacle = Game.instance.obstacles[i]; var collision :Boolean = lineIntersecsCircle(ahead, ahead2, obstacle); // "position" -     if (collision && (mostThreatening == null || distance(position, obstacle) < distance(position, mostThreatening))) { mostThreatening = obstacle; } } return mostThreatening; }
      
      





回避の力は、キャラクターの速度ベクトルに追加する必要があります。 前の記事で説明したように、すべての制御力を1つに結合して、キャラクターに作用するすべてのアクティブな動作を表す力を作成できます。



回避力の角度と方向が一定であれば、シークやワンダーなど、他の制御力に干渉しません。 通常の方法で回避がキャラクターの速度に追加されます。



 steering = nothing(); //  ,  "  " steering = steering + seek(); // ,    -  steering = steering + collisionAvoidance(); steering = truncate (steering, max_force) steering = steering / mass velocity = truncate (velocity + steering, max_speed) position = position + velocity
      
      





ゲームが更新されるたびにすべてのステアリング動作が再計算されるため、障害物がパスをブロックしている限り、回避力がアクティブになります。



障害物がahead



のベクトルのセグメントを横切るのを停止すると、回避力はゼロになり(効果はありません)、再計算されて別の脅威の障害物を回避します。 この結果、障害を回避できるキャラクターが得られます。






衝突認識の改善



現在の実装には、衝突認識に関連する2つの問題があります。 最初は、 ahead



ベクトルが障害物の範囲外にあるが、キャラクターが障害物に近すぎる(または内部)場合に発生します。



これが発生した場合、衝突が検出されないため、キャラクターは障害物に触れて(または進入して)回避プロセスをスキップします。









ahead



のベクトルが障害物の外側にある場合もありますが、キャラクターは内側にあります。




この問題は、衝突チェックに3番目のベクトル、つまりキャラクターの位置ベクトルを追加することで修正できます。 3つのベクトルを使用すると、衝突認識が大幅に向上します。



2番目の問題は、キャラクターが障害物に近づき、障害物から遠ざかるときに発生します。 キャラクターが単に他の方向を向いている場合でも、操作すると衝突することがあります。









キャラクターがちょうど回転している場合でも、操縦は衝突につながる可能性があります。



この問題は、キャラクターの現在の速度に応じてahead



のベクトルのスケールを変更することで解消できます。 たとえば、 ahead



ベクトルを計算するコードは次のように変更されます。



 dynamic_length = length(velocity) / MAX_VELOCITY ahead = position + normalize(velocity) * dynamic_length
      
      





dynamic_length



変数は0から1まで変化します。キャラクターがフルスピードで移動するとき、 dynamic_length



は1です。 文字が減速または加速する場合、 dynamic_length



は0以上(例:0.5)です。



したがって、キャラクターが移動せずに単に操作する場合、 dynamic_length



はゼロになる傾向があり、衝突のないゼロベクトルをahead



作成します。






デモ:ゾンビの時間!



障害物回避行動を実際に示すには、ゾンビの大群が最適です。 以下は、マウスカーソルをシーク(シーク)する複数のゾンビ(すべて速度が異なる)のデモです。





Flashのインタラクティブデモはこちらです。






おわりに



衝突回避動作により、すべてのキャラクターが環境内の障害物をかわすことができます。 ゲームが更新されるたびにすべての制御力が新たに再計算されるため、キャラクターはさまざまな障害物と問題なく対話し、常に最も脅威のある(最も近い)分析を行います。



この動作はパスを見つけるためのアルゴリズムではありませんが、密集したマップで得られる結果は非常に説得力があります。



パート7.パスをたどる



パスをたどるというタスクは、ゲーム開発でよく遭遇します。 このパートでは、ポイントとセグメントで構成される特定のパスをキャラクターがたどることができる、ステアリング動作のパスフォローイングを見ていきます。






はじめに



パス追跡動作は、いくつかの方法で実装できます。 Reynoldsの初期実装では、セグメントで構成されるパスが使用され、レール上の列車のように、文字が厳密にそれに続きます。



状況によっては、この精度は必要ありません。 キャラクターはセグメントに沿ってパスに沿って移動できますが、レールのようではなく、 スナップとして使用します。



このチュートリアルで紹介するフォローアップの実装は、レイノルズが提案した実装の簡略化です。 また、良い結果が得られますが、射影ベクトルのような重い数学計算にはあまり関係しません。






パスを設定する



パスは、セグメントで接続された一連のポイント(ノード)として定義できます。 曲線を使用してパスを記述することができますが、ポイントとセグメントを使用する方が簡単で、ほぼ同じ結果が得られます。



曲線を使用する必要がある場合、それらを多くの接続点に減らすことができます。









曲線と線分。



Path



クラスは、ルートを記述するために使用されます。 実際、クラスにはポイントのベクトルと、このリストを処理するためのいくつかのメソッドがあります。



 public class Path { private var nodes :Vector.<Vector3D>; public function Path() { this.nodes = new Vector.<Vector3D>(); } public function addNode(node :Vector3D) :void { nodes.push(node); } public function getNodes() :Vector.<Vector3D> { return nodes; } }
      
      





各ウェイポイントはVector3D



であり、これはキャラクターのposition



プロパティがどのように機能するかに似た空間内のposition



です。






ノードからノードへ移動



パスをナビゲートするために、キャラクターはルートの終点に到達するまでノードからノードに移動します。



パス上の各ポイントを目標と見なすことができるため、Seekの動作を使用できます。









あるポイントから別のポイントにシークを実行します。



キャラクターは、到達するまで現在のポイントに向かう傾向があり、次のウェイポイントが現在のポイントになります。 衝突の回避に関する部分で前述したように、各動作の力はゲームの更新ごとに再集計されます。つまり、あるノードから別のノードへの移行は、スムーズに、いつの間にか発生します。



ナビゲーションプロセスを処理するには、キャラクタークラスに2つの追加のプロパティが必要です。現在のノード(キャラクターが狙っているノード)と、それに続くパスへのリンクです。 クラスは次のようになります。



 public class Boid { public var path :Path; public var currentNode :int; (...) private function pathFollowing() :Vector3D { var target :Vector3D = null; if (path != null) { var nodes :Vector.<Vector3D> = path.getNodes(); target = nodes[currentNode]; if (distance(position, target) <= 10) { currentNode += 1; if (currentNode >= nodes.length) { currentNode = nodes.length - 1; } } } return null; } private function distance(a :Object, b :Object) :Number { return Math.sqrt((ax - bx) * (ax - bx) + (ay - by) * (ay - by)); } (...) }
      
      





pathFollowing()



メソッドは、パスpathFollowing()



強さを生成します。 彼は強さを作成しませんが、目標を正しく選択します。



path != null



テストは、キャラクターがパスをたどるかどうかを確認します。 その場合、 currentNode



プロパティをcurrentNode



して、ポイントのリストで現在のターゲット(キャラクターが目指すべきターゲット)を検索します。



現在のターゲットとキャラクターの位置の間の距離が10



未満の場合、これはキャラクターが現在のノードに到達したことを意味します。 これが発生すると、 currentNode



1ずつ増加するため、キャラクターはパス上の次のポイントに向かう傾向があります。 パス内のポイントが終了するまで、プロセスが繰り返されます。






力の計算と追加



キャラクターをパスの各ノードにプッシュするために使用される力は、シーク動作の力です。 対応するコードはすでにpathFollowing()



メソッドで選択されているため、このノードにキャラクターをプッシュする力を返す必要があります。



 private function pathFollowing() :Vector3D { var target :Vector3D = null; if (path != null) { var nodes :Vector.<Vector3D> = path.getNodes(); target = nodes[currentNode]; if (distance(position, target) <= 10) { currentNode += 1; if (currentNode >= nodes.length) { currentNode = nodes.length - 1; } } } return target != null ? seek(target) : new Vector3D(); }
      
      





パスをたどる強さを計算した後、通常どおり、キャラクターの速度ベクトルに追加する必要があります。



 steering = nothing(); //  ,  "  " steering = steering + pathFollowing(); steering = truncate (steering, max_force) steering = steering / mass velocity = truncate (velocity + steering, max_speed) position = position + velocity
      
      





パスをたどる制御力は、キャラクターがターゲットを捕まえるために常に方向を変える追跡の動作に非常に似ています。 違いは、キャラクターが動きのない目標を目指して努力するという事実にあります。目標に到達すると、無視して別の目標を目指します。



結果は次のようになります。





Flashのインタラクティブデモはこちらです。






モーションスムージング



現在の実装では、パス上の現在のポイントを「タッチ」して次のポイントを選択するには、すべてのキャラクターが必要です。 したがって、キャラクターは、たとえばターゲットに到達するまで円を描くようにターゲットの周りを移動するなど、望ましくない動きのパターンを実行できます。



自然界では、すべての動きは最小努力の原則に従う傾向があります。 たとえば、人は廊下の真ん中を常に移動するわけではありません。 前に曲がっている場合、壁に近づいて距離を縮めます。



このパターンは、パスに半径を追加することで再作成できます。 半径はポイントに適用され、ルートの「幅」と見なすことができます。 彼は、パスに沿って移動して、キャラクターがポイントからどれだけ移動できるかを制御します。









パスをたどる際の半径の影響。



キャラクターとポイント間の距離が半径以下である場合、ポイントに到達したと見なされます。 したがって、すべてのキャラクターは、セグメントおよびポイントをガイドとして使用して移動します





Flashのインタラクティブデモはこちらです。

半径が大きいほど、ルートが広くなり、ターン中にキャラクターが保持するポイントまでの距離が長くなります。 半径の値を変更して、さまざまな繰り返しパターンを作成できます。






前方および後方



場合によっては、パスの最後に到達すると、キャラクターが動き続けることが役立つ場合があります。 たとえば、パトロールパターンでは、キャラクターは最後に到達すると、同じポイントをたどってルートの最初に戻る必要があります。



これは、 pathDir



プロパティを文字クラスに追加することで実現できます。 文字がパスに沿って移動する方向を制御する整数値です。 pathDir



1



場合、キャラクターはパスの最後に移動します。 -1



は、先頭への移動を示します。



pathFollowing()



メソッドは次のように変更できます。



 private function pathFollowing() :Vector3D { var target :Vector3D = null; if (path != null) { var nodes :Vector.<Vector3D> = path.getNodes(); target = nodes[currentNode]; if (distance(position, target) <= path.radius) { currentNode += pathDir; if (currentNode >= nodes.length || currentNode < 0) { pathDir *= -1; currentNode += pathDir; } } } return target != null ? seek(target) : new Vector3D(); }
      
      





前のバージョンとは異なり、今ではpathDir



の値がcurrentNode



プロパティに追加されます(単に1



追加するのではなく)。 これにより、キャラクターは現在の方向に基づいてパス上の次のポイントを選択できます。



その後、テストはキャラクターがルートの終わりに到達したかどうかをチェックします。 その場合、 pathDir



-1



が乗算され、値が逆になり、キャラクターは動きの方向を逆にします。



その結果、前後に移動するパターンが得られます。






おわりに



パス追従動作により、キャラクターは指定されたパスに沿って移動できます。 ルートはポイントによって定義され、その幅を調整して、より自然に見える動きのパターンを作成できます。



このパートで説明する実装は、レイノルズによって提案されたパスの初期動作を単純化したものですが、それでももっともらしい自然な結果を生み出します。



パート8.リーダーに従う



キャラクター(またはキャラクターのグループ)は、パスをたどることができることに加えて、キャラクター(分隊長など)をたどることもできなければなりません。 この問題は、 リーダーの次の動作を使用して解決できます。






はじめに



リーダーを追跡する動作は、キャラクターのグループが特定のキャラクター(リーダー)を追跡するように配置された他の制御力の組み合わせです。 簡単なアプローチでは、シークまたは追跡の動作を使用してフォローパターンを作成できますが、結果はあまり良くありません。



シーク動作では、キャラクターはターゲットに向かって、遅かれ早かれターゲットと同じ場所に移動します。 一方、追跡行動は、キャラクターを別のキャラクターに押し付けますが、それを追いかけるだけでなく、予測に基づいてキャッチすることを目的としています。



リーダーをフォローするときのタスクは、リーダーの十分近くにいるが、 彼の少し後ろにいることです。 さらに、キャラクターが遠くにいるときは、リーダーに速く移動する必要がありますが、距離が短くなると遅くなります。 これは、3つのステアリング動作の組み合わせによって実現できます。





以下では、リーダーをフォローするためのパターンを作成するために、これらの各動作をどのように組み合わせることができるかを説明します。






フォローする適切なポイントを見つける



フォローの過程で、キャラクターは指揮官の後ろにいる軍隊のように、リーダーの少し後ろに留まるように努めるべきです。 フォローアップポイント( behind



と呼ばれる)は、キャラクターの方向も表すため、ターゲットの速度に基づいて簡単に計算できます。 擬似コードは次のとおりです。



 tv = leader.velocity * -1; tv = normalize(tv) * LEADER_BEHIND_DIST; behind = leader.position + tv;
      
      





速度ベクトルに-1



掛けると、結果は速度ベクトルの逆になります。 次に、この結果のベクトル( tv



と呼ばれる)を正規化、スケーリングし、キャラクターの現在位置に追加できます。



プロセスの視覚的表現は次のとおりです。









シーケンスポイントを見つけるために使用されるベクトル演算。



LEADER_BEHIND_DIST



が大きいほど、リーダーとその背後のポイント間の距離が大きくなります。 キャラクターはこのポイントをたどるので、リーダーから遠くなるほど、キャラクターは遠くなります。






フォローと到着



次のステップはキャラクターがリーダーポイントに続くことbehind



です。他のすべての動作と同様に、次のプロセスはメソッドによって生成される力によって制御されますfollowLeader()







 private function followLeader(leader :Boid) :Vector3D { var tv :Vector3D = leader.velocity.clone(); var force :Vector3D = new Vector3D(); //   behind tv.scaleBy(-1); tv.normalize(); tv.scaleBy(LEADER_BEHIND_DIST); behind = leader.position.clone().add(tv); //       behind force = force.add(arrive(behind)); return force; }
      
      





このメソッドはポイントbehind



計算し、そのポイントに到達する力を作成します。次に、followLeader



他のすべての動作と同様に、キャラクターの制御力に強さを追加できます。



 steering = nothing(); //  ,  "  " steering = steering + followLeader(); steering = truncate (steering, max_force) steering = steering / mass velocity = truncate (velocity + steering, max_speed) position = position + velocity
      
      





この実装の結果、キャラクターのグループがbehind



リーダーのポイントに到達できるようになりますFlashのインタラクティブデモ)。






混雑回避



リーダーに従うと、キャラクターが互いに近すぎて、結果が不自然に見えるかもしれません。すべてのキャラクターは同様の力の影響を受けるため、同じように動き、「束」を形成する傾向があります。このパターンは、群れ行動を導く規則の1つである分離の助けを借りて修正できます分離の力では、キャラクターのグループが積み重なり、互いの間に一定の距離を維持することはできません。分離力は次のように計算できます。







 private function separation() :Vector3D { var force :Vector3D = new Vector3D(); var neighborCount :int = 0; for (var i:int = 0; i < Game.instance.boids.length; i++) { var b :Boid = Game.instance.boids[i]; if (b != this && distance(b, this) <= SEPARATION_RADIUS) { force.x += b.position.x - this.position.x; force.y += b.position.y - this.position.y; neighborCount++; } } if (neighborCount != 0) { force.x /= neighborCount; force.y /= neighborCount; force.scaleBy( -1); } force.normalize(); force.scaleBy(MAX_SEPARATION); return force; }
      
      





次に、分離の力を力に追加して、リーダーを目指すと同時にfollowLeader



キャラクターを互いに引き離すことができます



 private function followLeader(leader :Boid) :Vector3D { var tv :Vector3D = leader.velocity.clone(); var force :Vector3D = new Vector3D(); //   behind tv.scaleBy(-1); tv.normalize(); tv.scaleBy(LEADER_BEHIND_DIST); behind = leader.position.clone().add(tv); //       behind force = force.add(arrive(behind)); //    force = force.add(separation()); return force; }
      
      





その結果、より自然な見た目のパターンが得られます(Flash デモ)。






邪魔にならない



リーダーが突然現在の方向を変えると、キャラクターが邪魔をする可能性があります。登場人物はリーダーに従うため、リーダーの前に彼を置いておくのは非論理的です。



キャラクターがリーダーの邪魔になった場合、彼はすぐに離れて道をクリアする必要があります。これは動作を回避することで実現できます。









リーダーの道を避け



、文字の視認性の分野でのリーダーは、我々は衝突回避の行動の障害物を認識する方法と似た概念、使用するかどうかをチェックするために:リーダーの現在の速度と方向に基づいてを、我々は(と呼ばれるその前にポイントを投影していますahead



)。ahead



たとえば30



リーダーのポイントとキャラクターの間の距離が短い場合、キャラクターはリーダーの視野内にあり、移動するはずです。



ポイントはahead



behind



ポイントと同じ方法で計算されます。違いは、速度ベクトルが反転しないことです。



 tv = leader.velocity; tv = normalize(tv) * LEADER_BEHIND_DIST; ahead = leader.position + tv;
      
      





followLeader()



文字がスコープ内にあるかどうかを確認するためにメソッドを変更する必要があります。これが発生した場合、値force



(戻りevade(leader)



値)、つまりリーダーの位置を回避する力が変数に追加されます(連続を表す)



 private function followLeader(leader :Boid) :Vector3D { var tv :Vector3D = leader.velocity.clone(); var force :Vector3D = new Vector3D(); //   ahead tv.normalize(); tv.scaleBy(LEADER_BEHIND_DIST); ahead = leader.position.clone().add(tv); //   behind tv.scaleBy(-1); behind = leader.position.clone().add(tv); //       ,    //     . if (isOnLeaderSight(leader, ahead)) { force = force.add(evade(leader)); } //      behind force = force.add(arrive(behind, 50)); // 50 -   //    force = force.add(separation()); return force; } private function isOnLeaderSight(leader :Boid, leaderAhead :Vector3D) :Boolean { return distance(leaderAhead, this) <= LEADER_SIGHT_RADIUS || distance(leader.position, this) <= LEADER_SIGHT_RADIUS; } private function distance(a :Object, b :Object) :Number { return Math.sqrt((ax - bx) * (ax - bx) + (ay - by) * (ay - by)); }
      
      





すべてのキャラクターは、一度リーダーの視界に入ると、現在の位置を即座に回避します(Flash デモ)。



ヒント:リーダーをフォローする力は、いくつかの力の組み合わせです。キャラクターが追従の力の影響を受ける場合、実際には到着、分離、回避の力の影響も同時に受けます。






デモ



以下は、リーダーをフォローする動作を示すデモです。青い兵士(リーダー)はマウスカーソルに関連して到着動作を実行し、緑の兵士はリーダーに従います。



プレイヤーが画面をクリックすると、すべての兵士がカーソルの方向に向きを変えて撃ちます。モンスターは数秒ごとに到着動作をランダムなポイントに適用します。





Flashのインタラクティブデモはこちらです。




おわりに



リーダーの追従動作により、キャラクターのグループは特定の目標をたどりながら、少し遅れをとることができます。さらに、キャラクターは、リーダーの邪魔をしている場合、リーダーの現在位置を避けます。



リーダーに従う行動は、到着、回避、分離などの他いくつかの行動の組み合わせであることに注意することが重要です。単純な動作を組み合わせて、非常に複雑なモーションパターンを作成できることを示しています。



パート9.キュー



部屋がAI制御のエンティティで満たされているゲームシーンを想像してください。何らかの理由で、彼らは部屋を出てドアを通らなければなりません。ランダムにお互いを通り抜けるのではなく、丁寧に並んでいることを教えましょう。



チュートリアルのこの部分では、群衆を動かし、エンティティのチェーンを作成するさまざまなアプローチで、キュー(キュー)の動作を理解します






はじめに



このチュートリアルのコンテキストでキューイングは、ある時点での到着を辛抱強く待っているキャラクターの行を作成するプロセスです。行の最初の人が移動すると、他の人が彼を追いかけ、彼の後ろにワゴンが伸びている電車に似たパターンを作成します。待機中、キャラクターはキューから離れることはできません。



キューの動作を説明し、さまざまな実装を実証するには、「キューのあるシーン」を使用したデモが最適です。良い例は、AI制御エンティティがドアから出て部屋から出ようとする部屋です(Flash デモ)。



このシーンは、前述の2つの動作、シークと衝突回避を使用して作成されました。



ドアは、2つの長方形の障害物で構成されており、その間に隙間(出入口)があります。キャラクターは、このドアの後ろにあるポイントを探します。そこに達すると、キャラクターは画面の下部に移動します。



キューの動作がなければ、シーンは野av人の大群のように見え、目的地に到着するのに苦労します。動作の実装後、群集は部屋をスムーズに離れ、列を作成します。






楽しみにして



キャラクターが並ぶ必要がある最初の能力は、誰かが彼の前にいるかどうかを調べる能力です。この情報に基づいて、彼は移動を続けるか停止するかを決定できます。



前の隣人の存在を確認するより複雑な方法が存在するにもかかわらず、私はポイントとキャラクターの間の距離に基づいて単純化された方法を使用します。このアプローチは、前方の障害物を確認するための衝突回避動作で使用されました。









前のポイントで隣人をチェックします。



名前のあるドットがキャラクターの前に投影されahead



ます。この点と隣接するキャラクターの間の距離が以下である場合MAX_QUEUE_RADIUS



、これは前に誰かがいてキャラクターが停止することを意味します。



ポイントはahead



次のように計算されます(擬似コード):



 //  qa,  ahead    qa = normalize(velocity) * MAX_QUEUE_AHEAD; ahead = qa + position;
      
      





文字の方向も与える速度は、とMAX_QUEUE_AHEAD



呼ばれる新しいベクトルを作成するために正規化およびスケーリングされますqa



qa



ベクトルに追加すると、position



結果はキャラクターMAX_QUEUE_AHEAD



からユニットの距離にあるキャラクターの前のポイントになります。



これはすべてメソッドでラップできますgetNeighborAhead()







 private function getNeighborAhead() :Boid { var i:int; var ret :Boid = null; var qa :Vector3D = velocity.clone(); qa.normalize(); qa.scaleBy(MAX_QUEUE_AHEAD); ahead = position.clone().add(qa); for (i = 0; i < Game.instance.boids.length; i++) { var neighbor :Boid = Game.instance.boids[i]; var d :Number = distance(ahead, neighbor.position); if (neighbour != this && d <= MAX_QUEUE_RADIUS) { ret = neighbor; break; } } return ret; }
      
      





このメソッドは、ポイントahead



と他のすべての文字との間の距離をチェックし、最初の文字を返しMAX_QUEUE_AHEAD



ます。距離は以下です文字が見つからない場合、メソッドはを返しますnull










キューイングメソッドの作成



他のすべての動作と同様に、キューイングパワーは次のメソッドで計算されqueue()



ます。



 private function queue() :Vector3D { var neighbor :Boid = getNeighborAhead(); if (neighbor != null) { // TODO:  ,      } return new Vector3D(0, 0); }
      
      





結果getNeighborAhead()



は変数に格納されますneighbor



の場合neighbor != null



、先に誰かがいます。それ以外の場合、パスは明確です。



方法queue()



、並びに行動のすべての他の方法は、方法自体に関連付けられた駆動力である力を返すべきです。それqueue()



は大きさなしで電力を返しますが、つまり、効果はありません。これまでドアのあるシーンのすべてのキャラクター



のメソッドupdate()



は、次のようになります(擬似コード)。



 public function update():void { var doorway :Vector3D = getDoorwayPosition(); steering = seek(doorway); // seek   steering = steering + collisionAvoidance(); //   steering = steering + queue(); //      steering = truncate (steering, MAX_FORCE); steering = steering / mass; velocity = truncate (velocity + steering , MAX_SPEED); position = position + velocity;
      
      





queue()



ゼロのパワーを返すため、キャラクターは行を作成せずに動き続けます。隣人が先に発見されたとき、彼らは何らかの行動を取る時です。






トラフィックの停止に関するいくつかの言葉



ステアリング動作は常に変化する力に基づいているため、システム全体が非常に動的になります。より多くの力が使用されるほど、特定の力のベクトルを特定してキャンセルすることがより難しくなります。



ステアリング動作に関するこの一連のチュートリアルで使用される実装では、すべての力が加算されます。したがって、力をキャンセルするには、力を再計算、変換し、現在の制御力のベクトルに追加し直す必要があります。



これは、キャラクターを停止させるために速度がキャンセルされる到着動作で発生することです。しかし、たとえば衝突回避、逃亡などの行動において、いくつかの力が一緒に作用するとどうなりますか?



以下のセクションでは、キャラクターを止める方法について2つのアイデアを提供します。1つ目は、速度ベクトルに直接作用し、他のすべての制御力を無視する「ハードストップ」アプローチを使用します。2番目は、と呼ばれる力ベクトルを使用して、brake



他のすべての制御力をキャンセルします。つまり、キャラクターを強制的に停止します。






ストップモーション:「ハードストップ」



いくつかの制御力は、キャラクターの速度ベクトルに基づいています。このベクトルが変化すると、他のすべての力に影響します。つまり、再計算する必要があります。「ハードストップ」のアイデアは非常に単純です。前にキャラクターがいる場合、速度ベクトルを「トリミング」します。



 private function queue() :Vector3D { var neighbor :Boid = getNeighborAhead(); if (neighbor != null) { velocity.scaleBy(0.3); } return new Vector3D(0, 0); }
      
      





上記のコードでは、文字が前にある場合、ベクトルスケールは現在の値(長さ)からvelocity



減少し30%



ます。その結果、動きは大幅に減少しますが、キャラクターがブロッキングパスを離れると最終的に通常の値に戻ります。



これは、各更新で動きがどのように計算されるかを分析することで理解しやすくなります



 velocity = truncate (velocity + steering , MAX_SPEED); position = position + velocity;
      
      





velocity



が低下し続ける場合、力にsteering



基づいているため、力これが発生しvelocity



ます。これは、極端に低い値をもたらす悪循環を作り出しますvelocity



そして、キャラクターは動きを止めます。



トリミングプロセスが完了すると、ゲームの更新ごとにベクトルvelocity



がわずかに増加し、強度にも影響しsteering



ます。徐々にいくつかの更新この意志ベクトルvelocity



steering



その正常値に。



「ハードストップ」ソリューションは、次の結果を作成します(フラッシュデモ)。



結果は非常に妥当ですが、それでも少し「機構的」に見えます。実際の群集では、通常、参加者の間に空きスペースはありません。






ストップモーション:制動力



動作を停止する2番目のアプローチでは、すべてのアクティブな制御力が強制的にキャンセルされるため、「機構的な」結果は少なくなりbrake



ます。



 private function queue() :Vector3D { var v :Vector3D = velocity.clone(); var brake :Vector3D = new Vector3D(); var neighbor :Boid = getNeighborAhead(); if (neighbor != null) { brake.x = -steering.x * 0.8; brake.y = -steering.y * 0.8; v.scaleBy( -1); brake = brake.add(v); } return brake; }
      
      





brake



アクティブな各制御力を再カウントおよび反転して力を作成する代わりに、現在適用されているすべての制御力を含むbrake



現在のベクトルに基づいて計算steering



されます。









抑制力の表現。



brake



受信コンポーネントx



y



強さはsteering



、それらを描画し、規模でそれらを変更します0.8



。これはbrake



、大きさが80%steering



で、反対方向を指しいること意味します。



ヒント:直接使用はsteering



危険です。これqueue()



がキャラクターに適用される最初の動作である場合、力steering



は「空」になります。したがって、他のすべての動作メソッドqueue()



を呼び出してから、彼が完全かつ最終的な力を獲得できるようにする必要がありますsteering







強さbrake



もキャラクターの速度を相殺するはずです。これは、-velocity



強度を追加することによって行われbrake



ます。この方法の後queue()



最終的な力を取り戻すことができbrake



ます。



制動力を使用した結果は次のようになります。





Flashのインタラクティブデモはこちらです。






文字オーバーレイの削減



すべてのキャラクターが空のスペースを埋めようとしているため、抑制のあるソリューションは「メカニズム」の結果と比較してより自然な結果を生み出します。ただし、新しい問題が発生します。文字が互いに重なり合っています。



それを排除するために、「ハードストップ」アプローチを少し修正したバージョンでブレーキソリューションを改善できます。



 private function queue() :Vector3D { var v :Vector3D = velocity.clone(); var brake :Vector3D = new Vector3D(); var neighbor :Boid = getNeighborAhead(); if (neighbor != null) { brake.x = -steering.x * 0.8; brake.y = -steering.y * 0.8; v.scaleBy( -1); brake = brake.add(v); if (distance(position, neighbor.position) <= MAX_QUEUE_RADIUS) { velocity.scaleBy(0.3); } } return brake; }
      
      





新しいテストは、最近傍をチェックするために使用されます。今回は、距離を測定するためにポイントを使用する代わりにahead



、新しいテストは文字ベクトル間の距離をチェックしますposition













MAX_QUEUE_RADIUSの半径内で、前方ではなく位置を中心とする最近傍をチェックします。



この新しいテストは半径の中にどんな最も近い隣人がいるかチェックしますMAX_QUEUE_RADIUS



が、今それはベクトルの中央にありposition



ます。内部に誰かがいる場合、これは周囲の領域がいっぱいになりすぎており、キャラクターが重なり始める可能性があることを意味します。



オーバーレイは、ベクトルvelocity



を現在の値の30%に更新するたびにスケーリングすることで削減できます。「ハードストップ」アプローチと同様に、ベクトルをトリミングすると、velocity



動きが大幅に減少します。



結果は「機構的」ではないように見えますが、キャラクターはまだ戸口で交差しているため、まだ不完全です(デモ Flashで)。






分離を追加する



キャラクターは説得力のある方法で戸口に到達しようとしますが、通路が狭くなるとすべての空きスペースを埋めようとしますが、戸口では互いに近づきすぎます。



この問題は、分離力を追加することで解決できます。



 private function queue() :Vector3D { var v :Vector3D = velocity.clone(); var brake :Vector3D = new Vector3D(); var neighbor :Boid = getNeighborAhead(); if (neighbor != null) { brake.x = -steering.x * 0.8; brake.y = -steering.y * 0.8; v.scaleBy( -1); brake = brake.add(v); brake = brake.add(separation()); if (distance(position, neighbor.position) <= MAX_QUEUE_RADIUS) { velocity.scaleBy(0.3); } } return brake; }
      
      





リーダー追従動作で以前使用されていた分離力がフォースに追加されbrake



、キャラクターが停止すると同時に、互いに触れないようにします。



その結果、戸口から抜け出そうとする信頼できる群衆が得られます。





Flashのインタラクティブデモはこちらです。




おわりに



キューの動作により、キャラクターは一列に並び、目的地に到着するまで辛抱強く待つことができます。並んでいる間、キャラクターは位置を飛び越えて「チート」しようとしない。彼は自分の前に立っているキャラクターが動いたときにのみ動きます。



このチュートリアルに示されている戸口のシーンは、この動作がいかに多用途でカスタマイズ可能であるかを示しています。わずかな変更でもまったく異なる結果が作成され、さまざまな状況に合わせて簡単に調整できます。この動作は、衝突回避など、他の動作と組み合わせることもできます。



この新しい振る舞いを楽しんでいただき、ゲームで感動的な群衆を作成するためにそれを使用することを願っています!



All Articles