
ステアリング動作は、単純な力を使用することで自律的なキャラクターが現実的に動くのを助けます。単純な力の組み合わせにより、環境の周りで自然な見た目と即興の動きが作成されます。 このチュートリアルでは、ステアリング動作理論の基礎とその実装について説明します。
そのような行動が構築されるアイデアは、 クレイグ・ラインドルズによって提案されています。 それらは、経路計画やグローバルコンピューティングを使用した複雑な戦略に基づいているのではなく、近隣のオブジェクトの強度などのローカル情報を適用します。 このおかげで、理解と実装が簡単ですが、同時に非常に複雑な動きのパターンを作成できます。
注:このチュートリアルはAS3とFlashで作成されていますが、ほぼすべてのゲーム開発環境で同じ手法と概念を使用できます。 数学的なベクトルの基本的な理解が必要になります。
パート1.シーク動作(検索)
位置、速度および動き
数学的ベクトルを使用して、ステアリング動作に関係するすべての力を実現できます。 これらの力はキャラクターの速度と位置に影響するため、ベクトルを使用してそれらを表すことも論理的です。
ベクトルには方向がありますが 、位置に関しては無視します(位置ベクトルはキャラクターの現在位置を示すと仮定します)。

上の画像は、
(x, y)
位置し
(x, y)
速度
(a, b)
を持つ文字を示しています。 運動はオイラー法を使用して計算されます:
position = position + velocity
速度ベクトルの方向はキャラクターの動きの方向を制御し、その長さ(または大きさ)は各フレームの動きの量を制御します。 長さが長いほど、キャラクターは速く動きます。 速度ベクトルは、特定の値を超えないように切り捨てることができます(通常、これは最大速度です)。 以下は、このアプローチを示す画像です。
赤い正方形がターゲットに向かって移動します。 モーションパターンは、 seekの動作を示していますが、これまでのところ、制御力が適用されていません 。 緑の線は、次のように計算された速度ベクトルを示します。
velocity = normalize(target - position) * max_velocity
制御力がなければ、キャラクターは直接のルートを記述し、ターゲットを移動するとすぐに方向を変えるため、現在のルートと新しいルートの間で急激な移行を行うことに注意してください。
力の計算
速度の力のみが関係する場合、キャラクターはこのベクトルの方向によって定義される直線でのみ移動します。 ステアリング動作の原則の1つは、力( 制御力と呼ばれる)を追加してキャラクターの動きに影響を与えることです。 これらの力のおかげで、キャラクターはいずれかの方向に移動します。
シーク動作の場合、各フレームのキャラクターに制御力を追加することで、彼はスムーズに速度を変更し、ルートの突然の変更を回避します。 ターゲットが移動すると、キャラクターは徐々に速度ベクトルを変更し、新しい場所でターゲットに到達しようとします。
シーク動作には、 必要な速度と制御力の2つの力が関係しています。

必要な速度は、可能な限り最短のパスに沿ってキャラクターをゴールに導く力です(それらの間の直線で-以前はこれがキャラクターに作用する唯一の力でした)。 駆動力は、必要な速度から現在の速度を引いた結果です。 彼女はまた、キャラクターをゴールに向けます。
これらの力は次のように計算されます。
desired_velocity = normalize(target - position) * max_velocity steering = desired_velocity - velocity
強制適用
制御力を計算した後、それをキャラクターに適用する必要があります(速度の力に追加されます)。 フレームごとに制御力を追加すると、キャラクターはスムーズに古い直接ルートを離れて、目標に向かって進み、検索パスを説明します (下図のオレンジの曲線)。

これらの力の適用と最終的な速度/位置の計算は次のとおりです。
steering = truncate (steering, max_force) steering = steering / mass velocity = truncate (velocity + steering , max_speed) position = position + velocity
制御力は、キャラクターが処理できる許容力の量を超えないように切り捨てられます。 さらに、制御力はキャラクターの質量で除算されます。これにより、ウェイトが異なるキャラクターに対して異なる速度が作成されます。
ターゲットが移動するたびに、それに応じて各キャラクターに必要な速度のベクトルが変化します。 ただし、 速度ベクトルを変更し、再びターゲットに向けるには時間がかかります。 その結果、動きがスムーズに移行します。
パート2。逃亡と到着の行動
逃げる
上記のシーク動作は、キャラクターをターゲットに向ける2つの力、つまり必要な速度と制御力に基づいています。
desired_velocity = normalize(target - position) * max_velocity steering = desired_velocity - velocity
この場合、
desired_velocity
は、キャラクターとターゲットの間の最短パスです。 キャラクターの位置から目標位置を引くことで得られます。 結果は、 キャラクターからターゲットに渡される力ベクトルです。

逃亡は同じ2つの力を使用しますが、キャラクターがターゲットから逃げるように設定されています。

逃亡行動
この新しい
desired_velocity
ベクトル
desired_velocity
、 ターゲットの位置からキャラクターの位置を減算することによって計算され、 ターゲットからキャラクターに向けられたベクトルが得られます。
結果の力は、シーク動作とほぼ同じ方法で計算されます。
desired_velocity = normalize(position - target) * max_velocity steering = desired_velocity - velocity
この場合、
desired_velocity
は、キャラクターがターゲットからエスケープするために使用できる最短のエスケープパスを表します。 制御力により、キャラクターは現在のルートを離れ、希望する速度のベクトルの方向にキャラクターを押し出します。
fleeの動作で必要な速度のベクトルを、seekの動作で同じベクトルと比較すると、次の関係を導き出すことができます。
flee_desired_velocity = -seek_desired_velocity
言い換えると、1つのベクトルは別のベクトルに対して負です。
回避
制御力を計算した後、それをキャラクターの速度ベクトルに追加する必要があります。 この力はキャラクターをターゲットから遠ざけるため、キャラクターは各フレームでターゲットに向かって動きを止め、そこから離れ始め、 エスケープパスを作成します (下図のオレンジの曲線)。

これらの力の追加と最終速度/位置の計算は、以前と同じ方法で実行されます。
すべての力を適用すると、各キャラクターは現在のルートをスムーズに離れ、目標を回避できます。
ゴールは各キャラクターに影響しますが、キャラクター間の距離は考慮しません。 「影響範囲」を制限して、キャラクターがターゲットに近すぎる場合にのみ逃げるようにすることができます。
到着
これまで見てきたように、シーク動作はキャラクターをゴールに向かって移動させます。 彼が目標に達すると、支配力は同じルールに従って彼に影響を与え続け、キャラクターをターゲットの前後に「ジャンプ」させます。
到着時の動作では、キャラクターがターゲットを通過することはできません。 終点に近づくと、キャラクターが目標に到達するとスローダウンして停止します。
この動作は2つのステップで構成されています。 最初の段階は、キャラクターがターゲットから遠いときです。 シーク動作と同じように機能します(キャラクターは最大速度でターゲットに向かって移動します)。
第2段階は、キャラクターがターゲットの近くにあり、その「減速領域」(ターゲットの位置を中心とする円)内にあるときに開始されます。

キャラクターが円に入ると、ターゲットで止まるまでスローダウンします。
スローダウン
キャラクターがスローダウンの領域に入ると、その速度はゼロまで直線的に減少します。 これは、新しい制御力( 到着力 )をキャラクターの速度ベクトルに適用することで実現できます。 この加算の結果は遅かれ早かれゼロに等しくなります。つまり、各フレームでキャラクターの位置に何も追加されません(つまり、動きはありません)。
// (velocity + steering) , velocity = truncate(velocity + steering, max_speed) position = position + velocity function truncate(vector:Vector3D, max:Number) :void { var i :Number; i = max / vector.length; i = i < 1.0 ? i : 1.0; vector.scaleBy(i); }
キャラクターが停止する前にゆっくりと減速するために、速度がすぐにゼロに低下することはありません。 段階的な減速のプロセスは、減速領域の半径と、キャラクターとターゲットの間の距離に基づいて計算されます。
// desired_velocity = target - position distance = length(desired_velocity) // , // if (distance < slowingRadius) { // desired_velocity = normalize(desired_velocity) * max_velocity * (distance / slowingRadius) } else { // desired_velocity = normalize(desired_velocity) * max_velocity } // steering = desired_velocity - velocity
距離が
slowingRadius
よりも大きい場合、キャラクターはターゲットから遠く、彼の速度は
max_velocity
等しいままである必要があります。
距離が
slowingRadius
より小さい場合、キャラクターはスローダウンの領域に入り、スピードが低下します。
distance / slowingRadius
値の範囲は、
1
(
distance
が
slowingRadius
場合)から
0
(
distance
ほぼゼロの場合)です。 線形に変化すると、速度が徐々に低下します。

上記のように、キャラクターの動きは次のとおりです。
steering = desired_velocity - velocity velocity = truncate (velocity + steering , max_speed) position = position + velocity
必要な速度がゼロまで低下した場合、制御力は
-velocity
等しくなり
-velocity
。 したがって、この制御力が速度に追加されると、ゼロになり、キャラクターが停止します。
基本的に、到着時の動作は
-velocity
に等しい力を計算し、その力が適用されている間はキャラクターが動くことを許可しません。 初期のキャラクター速度ベクトルは変化せず動作し続けますが、制御力を加えることでゼロにリセットされます。
到着の制御力が取り除かれると、キャラクターは元の速度ベクトルを使用して再び動き始めます。
おわりに
逃げる行動はキャラクターをターゲットから遠ざけ、到着する行動はキャラクターをスローダウンさせ、ターゲットの位置で停止させます。 両方の動作を使用して、スムーズなエスケープを作成したり、パターンに従うことができます。 さらに、それらを組み合わせて、さらに複雑な動きを作成できます。
パート3.行動のふらつき
さまよう
ゲームでは、キャラクターがランダムに歩き回ることがしばしば必要です。 通常、そのようなキャラクターは、何らかの種類のイベント(たとえば、プレイヤーとの戦い)を単に待っているか、何かを探しています。 プレイヤーがこの動作を見ることができる場合、キャラクターの放浪機能は非常に現実的で美しく見えるはずです。
プレイヤーが明確に定義されたルートまたは非現実的な動きを見ると、これは迷惑につながります。 最悪の場合、プレイヤーはキャラクターの動きを予測する方法を理解し、ゲームプレイは退屈になります。
ワンダーコントロールの動作は 、キャラクターが実際に生きて独立して歩いていることをプレイヤーに確信させる現実的な「自然な」動きを作成するように設計されています。
検索とランダム性
ステアリング動作を使用してさまようパターンを実装するには、いくつかの方法があります。 最も簡単なのは、前述のシーク動作です。 キャラクターが検索すると、彼はゴールに向かって動きます。
この目標の位置が数秒ごとに変化する場合、キャラクターはそれを達成することはできません(そして可能であれば、目標は再び移動します)。 プレイエリアにターゲットをランダムに配置すると、キャラクターは環境を動き回り、ターゲットを追いかけます。
Flashのインタラクティブデモはこちらです。
これは、次のコードで実装できます。
// private function wander() :Vector3D { var now :Number = (new Date()).getTime(); if (now >= nextDecision) { // "" } // , // ( seek) return seek(target); } // // , : public function update() :void { steering = wander() steering = truncate (steering, max_force) steering = steering / mass velocity = truncate (velocity + steering , max_speed) position = position + velocity }
これはシンプルで優れたアプローチですが、最終結果は完全に信じられるとは限りません。 ターゲットが彼の後ろに配置されるため、キャラクターがルートを完全に変更する場合があります。 それから、キャラクターの行動は「いまいましい、キーを忘れてしまった!」として認識されます。 「では、今、私はこの方向に行きます 。 」
さまよう
クレイグレイノルズがこれらの行動を発明したときに、ワンダー行動の別の実装が提案されました。 主なアイデアは、小さなランダムオフセットを作成し、各ゲームフレームでそれらをキャラクターの現在の方向のベクトルに適用することです(この場合、速度を上げるため)。 速度ベクトルはキャラクターの動きの方向と動きの速度を決定するため、このベクトルへの影響は現在のルートの変更につながります。
各フレームで小さなオフセットを使用すると、キャラクターのルートの突然の変更を回避できます。 たとえば、キャラクターが上に移動して右に曲がった場合、ゲームの次のフレームでは、彼はまだ上に移動して右に曲がりますが、角度はわずかに異なります。
このアプローチは、さまざまな方法で実装することもできます。 それらの1つは、キャラクターの前に円を配置し、それを使用して作用するすべての力を計算することです。

変位力の開始点は円の中心にあり、その半径によって制限されます。 キャラクターから円までの半径と距離が大きいほど、ゲームの各フレームでプレーヤーが受ける勢いが強くなります。
この変位力は、キャラクターのルートに影響を与えるために使用されます。 放浪の力を計算するのに使用されます。
円の位置計算
さまよう力を計算するために必要な最初の要素は、円の中心の位置です。 円はキャラクターの前に配置する必要があるため、速度ベクトルをガイドとして使用できます。
// CIRCLE_DISTANCE - // , - . // : var circleCenter :Vector3D; circleCenter = velocity.clone(); circleCenter.normalize(); circleCenter.scaleBy(CIRCLE_DISTANCE);
circleCenter
ベクトルは、速度ベクトルのクローン(コピー)です。つまり、同じ方向を指します。 正規化され、スカラー値(この場合は
CIRCLE_DISTANCE
)が乗算され、次のベクトルが得られます。

変位力
次の要素は変位力であり、これは左右に曲がる役割を果たします。 この力は偏差の作成に使用されるため、どこにでも向けることができます。 y軸に揃えられたベクトルを使用してみましょう:
var displacement :Vector3D; displacement = new Vector3D(0, -1); displacement.scaleBy(CIRCLE_RADIUS); // // // setAngle(displacement, wanderAngle); // // wanderAngle, // // . wanderAngle += (Math.random() * ANGLE_CHANGE) - (ANGLE_CHANGE * .5);
バイアス力が作成され、円の半径によってスケーリングされます。 上記のように、半径が大きいほど、放浪の力が強くなります。
wanderAngle
は、変位力の「勾配」の大きさを決定するスカラー値です。 それを使用した後、ランダムな値がそれに追加され、ゲームの次のフレームで異なるようになります。 これにより、移動に必要なランダム性が作成されます。
これを理解するために、上記で計算された変位力が円の中心にあると想像しましょう。 円の半径の値によってスケーリングされるため、次のようになります。

ヒント:数学ベクトルは空間内に位置を持たず、方向と大きさ(長さ)のみを持つことを忘れないでください。 したがって、それらはどこにでも配置できます。
さまよう力
円の中心と変位ベクトルを計算した後、それらを組み合わせて放浪力を取得する必要があります。 この力は、次の2つのベクトルを追加して計算されます。
var wanderForce :Vector3D; wanderForce = circleCenter.add(displacement);
視覚的には、これらの力を次のように表すことができます。

放浪の力は、キャラクターから円の円周上の点に発するベクトルとして表すことができます。 このポイントの位置に応じて、放浪の力がキャラクターを左右に強くまたは弱く押します。

さまよう力が速度ベクトルと一致するほど、キャラクターは現在のルートを変更しなくなります。 放浪の力は、シークと逃げる力とまったく同じように機能します。キャラクターを正しい方向に押し出します。
シークと逃げの動作における力の方向がターゲットに基づいて計算されるように、さまよう方向は円の円周上のランダムなポイントに基づいて計算されます。 最終的なさまようコードは次のようになります。
private function wander() :Vector3D { // var circleCenter :Vector3D; circleCenter = velocity.clone(); circleCenter.normalize(); circleCenter.scaleBy(CIRCLE_DISTANCE); // // var displacement :Vector3D; displacement = new Vector3D(0, -1); displacement.scaleBy(CIRCLE_RADIUS); // // // setAngle(displacement, wanderAngle); // // wanderAngle, // // . wanderAngle += Math.random() * ANGLE_CHANGE - ANGLE_CHANGE * .5; // // var wanderForce :Vector3D; wanderForce = circleCenter.add(displacement); return wanderForce; } public function setAngle(vector :Vector3D, value:Number):void { var len :Number = vector.length; vector.x = Math.cos(value) * len; vector.y = Math.sin(value) * len; }
力の追加
さまよえる力を計算した後、キャラクターの動きに影響を与えることができるように、キャラクターの速度に加えなければなりません。 この力の追加は、以前と同じ方法で実行されます。
steering = wander() steering = truncate (steering, max_force) steering = steering / mass velocity = truncate (velocity + steering , max_speed) position = position + velocity
さまよえる力は、上記の行動と同じようにキャラクターのルートに影響を与えます。 違いは、ゲームの各フレームでキャラクターをランダムな方向に押すことです。
Flashのインタラクティブデモはこちらです。
おわりに
ふらつきの振る舞いは、ランダムな動きを実装する素晴らしい方法です。 キャラクターの前にある想像上の円によって制御され、必要な動きのパターンを作成するために変更できます。
パート4.追跡と回避
追跡とは何ですか?
追跡とは、目標をキャッチしたいという欲求に従うプロセスです。 ここでの全体の違いはキャッチという言葉であることに注意することが重要です。 オブジェクトが単純に目標をたどる場合、ターゲットの動きを繰り返すだけで十分なので、目標をたどります。
誰かを追跡する場合、追跡者はターゲットを追跡する必要がありますが、ターゲットが近い将来どこにあるかを予測する必要もあります。 次の数秒でターゲットの位置を予測(または評価)できる場合は、現在のパスを変更して不要なルートを回避できます。

未来を予測する
チュートリアルの最初の部分で述べたように、動きはオイラー法を使用して計算されます:
position = position + velocity
このことから、キャラクターの現在の位置と速度がわかっている場合、
T
ゲームの更新を通じて自分がどこにいるのかを予測することができます。 キャラクターが直線的に移動し、予測したい位置が3回の更新(
T=3
)後に配置されたとします。 すると、キャラクターの将来の位置は次のようになります。
position = position + velocity * T
正しい予測を行うには、
T
正しい値を選択する必要があります
T
値が大きすぎる場合、追跡者はゴーストを追いかけます。
T
ゼロに近すぎる場合、追跡者は実際には追跡せず、単に目標を追跡します(予測なし)。
予測がゲームの各フレームに対して計算される場合、目標が常に方向を変えていても、これは機能します。 各更新は、キャラクターの現在の速度ベクトルに基づいて新しい「将来の位置」を生成します(これは移動の方向も制御します)。
追求する
追跡の動作は、Seekとほぼ同じように機能します。唯一の違いは、追跡者が目標そのものではなく、近い将来の目標を追求することです。
ゲームのすべてのキャラクターがクラスによって表されるとし
Boid
ます。次に、次の擬似コードで、追跡動作の基本的な考え方を実装します。
public function pursuit(t :Boid) :Vector3D { T :int = 3; futurePosition :Vector3D = t.position + t.velocity * T; return seek(futurePosition); }
追跡力を計算した後、これまでのすべての制御力と同様に、速度ベクトルに追加する必要があります。
public function update() :void { steering = pursuit(target) steering = truncate (steering, max_force) steering = steering / mass velocity = truncate (velocity + steering , max_speed) position = position + velocity }
次の図は、このプロセスを示しています。

追跡者(以下のキャラクター)は、オレンジ色の曲線で示される経路をたどって、ターゲットの将来の位置を探します。完成した結果を以下に示します。ここでは、行動追跡で使用され
T=30
ます。
Flashのインタラクティブデモはこちらです。
追跡精度の改善
値が
T
一定の場合、問題が発生します。ターゲットが近づくと追跡の精度が低下します。これは、目標が近い場合、追跡はターゲットの予測位置(
T
「前方」のフレーム上)に従うためです。
実際の迫害では、追跡者はすでに目標に十分に近づいており、もはやその位置を予測するべきではないと認識しているため、このような動作は発生しません。
この問題を回避し、追跡の精度を大幅に高めることができる簡単なトリックがあります。考え方は非常に単純です。定数値の代わりに、
T
動的を使用する必要があります。
T = distanceBetweenTargetAndPursuer / MAX_VELOCITY
新しい値は
T
、2人のキャラクター間の距離とゴールが到達できる最大速度に基づいて計算されます。簡単に言えば、新しいと
T
は、「ターゲットが現在の位置から追跡者の位置に移動するために必要な更新の数」を意味します。
距離が大きくなると、距離は大きくなります
T
。つまり、追跡者はターゲットのはるか前方に位置する傾向があります。距離が短くなると、距離は小さくなります
T
。つまり、ターゲットに非常に近いポイントになります。この実装の新しいコードは次のようになります。
public function pursuit(t :Boid) :Vector3D { var distance :Vector3D = t.position - position; var T :int = distance.length / MAX_VELOCITY; futurePosition :Vector3D = t.position + t.velocity * T; return seek(futurePosition); }
追跡動作ではdynamicを使用します
T
。
Flashのインタラクティブデモはこちらです。
逃げる
Evadeの動作は、Pursuitの動作の反対です。ターゲットの将来の位置を探すのではなく、Evadeは動作中にこの位置から逃げます。

回避コードはほぼ同じで、最後の行のみが変更されます。
public function evade(t :Boid) :Vector3D { var distance :Vector3D = t.position - position; var updatesAhead :int = distance.length / MAX_VELOCITY; futurePosition :Vector3D = t.position + t.velocity * updatesAhead; return flee(futurePosition); }
Flashのインタラクティブデモはこちらです。
おわりに
この部分では、ハンターから逃げようとする動物の群れなど、さまざまなパターンを模倣するのに最適な追跡と回避の動作に注目しました。
パート5.モーションマネージャー
ステアリング動作は、リアルなモーションパターンを作成するのに最適ですが、それらを制御、使用、および組み合わせることができればさらに良いでしょう。このパートでは、前述のすべての動作のモーションマネージャーの実装について説明し、検討します。
コンビネーションステアリングフォース
前述のように、各ステアリング動作は、速度ベクトルに追加される正味の力(「制御力」と呼ばれる)を作成します。この力の方向と大きさはキャラクターに指示を与え、パターン(シーク、逃げる、放浪など)に従ってキャラクターを移動させます。一般的に、計算は次のようになります。
steering = seek(); // steering = truncate (steering, max_force) steering = steering / mass velocity = truncate (velocity + steering , max_speed) position = position + velocity
制御力はベクトルであるため、他のベクトル(および速度)に追加できます。しかし、本当の「魔法」は、さまざまな制御力を追加できることです。これで十分です:
steering = nothing(); // , " " steering = steering + seek(); steering = steering + flee(); (...) steering = truncate (steering, max_force) steering = steering / mass velocity = truncate (velocity + steering , max_speed) position = position + velocity
制御力を追加すると、これらすべての力を表すベクトルが得られます。上記のコードスニペットでは、結果として生じる制御力により、キャラクターに何かを強制的に検索させ、他の何かを回避させます。
以下は、単一の制御力を作成する制御力の組み合わせの例です。

複雑なパターンを簡単に作成できます
制御力を組み合わせることにより、非常に複雑なモーションパターンを簡単に作成できます。キャラクターが特定のエリアを避けながら何かを目指して努力するベクトルと力の助けを借りてコードを書くことがどれほど難しいか想像できますか?
距離、面積、経路、グラフ、その他の同様の量を計算する必要があります。オブジェクトが移動する場合、環境は常に変化しているため、これらすべての計算を毎回繰り返す必要があります。
ステアリング動作では、すべての力は動的です。ゲームが更新されるたびに計算する必要があるため、環境の変化に自然に対応することが理解されています。
モーションマネージャー
複数のステアリング動作を同時に使用するには、モーションマネージャーを作成すると便利です。アイデアは、既存のエンティティに接続できる「ブラックボックス」を記述することです。これにより、これらの動作を実行できます。
マネージャーには、接続先のエンティティ(「ホスト」)へのリンクがあります。マネージャーはホストにような方法、一連の送信
seek()
とを
flee()
。これらのメソッドが呼び出されるたびに、マネージャーはその内部プロパティを更新して制御力ベクトルを作成します。
すべての呼び出しを処理した後、マネージャーは結果の制御力をホスト速度ベクトルに追加します。これにより、アクティブな動作に応じてホスト速度ベクトルの大きさと方向が変更されます。
以下の図は、アーキテクチャを示しています。

要素を要約する
マネージャーには一連のメソッドがあり、それぞれが固有の動作を表します。各動作が機能するためには、さまざまな外部情報を送信する必要があります。
たとえば、シークの動作には、その場所に向けられた制御力の計算に使用される空間内のポイントが必要です。追跡には、現在の位置や速度など、ターゲットからのいくつかの情報が必要です。空間内のポイントは、
Point
またはのインスタンスとして表現できます
Vector2D
。どちらも、どのフレームワークでもかなり標準的なクラスです。
ただし、Pursuitの動作の目標は何でもかまいません。モーションマネージャーを十分に一般化するには、そのタイプに関係なく、いくつかの「質問」に答えることができる目標を取得する必要があります。たとえば、現在の速度は?「オブジェクト指向プログラミングのいくつかの原則のおかげで、これはインターフェイスを使用して実現できます。
インターフェイス
IBoid
は、モーションマネージャが制御できるエンティティを記述し、ゲーム内のすべてのクラスが実装する場合、ステアリング動作を使用できると仮定します。
IBoid
このインターフェイスは次の構造を持ちます。
public interface IBoid { function getVelocity() :Vector3D; function getMaxVelocity() :Number; function getPosition() :Vector3D; function getMass() :Number; }
モーションマネージャーの構造
マネージャーが一般化された形式でゲームのすべてのエンティティとやり取りできるようになったので、その基本構造を作成できます。マネージャーは、2つのプロパティ(結果の制御力とホストへのリンク)と、各動作に対応するパブリックメソッドのセットで構成されます。
public class SteeringManager { public var steering :Vector3D; public var host :IBoid; // public function SteeringManager(host :IBoid) { this.host = host; this.steering = new Vector3D(0, 0); } // API ( ) public function seek(target :Vector3D, slowingRadius :Number = 20) :void {} public function flee(target :Vector3D) :void {} public function wander() :void {} public function evade(target :IBoid) :void {} public function pursuit(target :IBoid) :void {} // . // public function update() :void {} // . public function reset() :void {} // API private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D {} private function doFlee(target :Vector3D) :Vector3D {} private function doWander() :Vector3D {} private function doEvade(target :IBoid) :Vector3D {} private function doPursuit(target :IBoid) :Vector3D {} }
マネージャーのインスタンスを作成するとき、接続先のホストへのリンクを受け取る必要があります。マネージャは、アクティブな動作に応じてホスト速度ベクトルを変更できます。
各動作は、パブリックとプライベートの2つのメソッドで表されます。たとえば、Seekの動作を考えてみましょう。
public function seek(target :Vector3D, slowingRadius :Number = 20) :void {} private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D {}
Public
seek()
は、この特定の動作を適用するようマネージャーに指示するために呼び出されます。このメソッドには戻り値がなく、そのパラメーターは動作自体(たとえば、空間内のポイント)に関連付けられています。privateメソッドはinside
doSeek()
で呼び出され、その戻り値、つまり この特定の動作の計算された制御力は、
steering
マネージャーのプロパティに追加されます。
次のコードは、seekの実装を示しています。
// . // slowingRadius ( Arrival). public function seek(target :Vector3D, slowingRadius :Number = 20) :void { steering.incrementBy(doSeek(target, slowingRadius)); } // Seek ( Arrival) private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D { var force :Vector3D; var distance :Number; desired = target.subtract(host.getPosition()); distance = desired.length; desired.normalize(); if (distance <= slowingRadius) { desired.scaleBy(host.getMaxVelocity() * distance/slowingRadius); } else { desired.scaleBy(host.getMaxVelocity()); } force = desired.subtract(host.getVelocity()); return force; }
他のすべての動作は、非常に類似した方法で実装されます。たとえば、メソッド
pursuit()
は次のようになります。
public function pursuit(target :IBoid) :void { steering.incrementBy(doPursuit(target)); } private function doPursuit(target :IBoid) :Vector3D { distance = target.getPosition().subtract(host.getPosition()); var updatesNeeded :Number = distance.length / host.getMaxVelocity(); var tv :Vector3D = target.getVelocity().clone(); tv.scaleBy(updatesNeeded); targetFuturePosition = target.getPosition().clone().add(tv); return doSeek(targetFuturePosition); }
チュートリアルの前の部分のコードを使用できます。行うための唯一のこと-の形でそれを適応させる
behavior()
と
doBehavior()
、それはトラフィックマネージャに追加することができます。
制御力の適用と更新
動作のメソッドが呼び出されるたびに、それによって計算された結果の力が
steering
マネージャーのプロパティに追加されます。したがって、このプロパティにはすべての制御力が蓄積されます。
すべての動作を呼び出した後、マネージャは、現在の制御力をホスト速度に適用して、アクティブな動作に従って移動するようにする必要があります。これは、
update()
モーションマネージャーメソッドで行われます。
public function update():void { var velocity :Vector3D = host.getVelocity(); var position :Vector3D = host.getPosition(); truncate(steering, MAX_FORCE); steering.scaleBy(1 / host.getMass()); velocity.incrementBy(steering); truncate(velocity, host.getMaxVelocity()); position.incrementBy(velocity); }
上記のメソッドは、すべての動作を呼び出した後、ホスト(またはゲームの他のエンティティ)によって呼び出される必要があります。そうしないと、ホストはアクティブな動作に一致するように速度ベクトルを変更できません。
申込み
Prey
ステアリング動作を使用して移動する必要があるクラスがあるが、これまでのところ、制御コードもモーションマネージャーもありません。その構造は次のようになります。
public class Prey { public var position :Vector3D; public var velocity :Vector3D; public var mass :Number; public function Prey(posX :Number, posY :Number, totalMass :Number) { position = new Vector3D(posX, posY); velocity = new Vector3D(-1, -2); mass = totalMass; x = position.x; y = position.y; } public function update():void { velocity.normalize(); velocity.scaleBy(MAX_VELOCITY); velocity.scaleBy(1 / mass); truncate(velocity, MAX_VELOCITY); position = position.add(velocity); x = position.x; y = position.y; } }
この構造により、クラスインスタンスはオイラーメソッドを使用して移動できます。マネージャーを使用できるように、クラスにはモーションマネージャーを参照するプロパティが必要であり、インターフェイスも使用する必要があります
IBoid
。
public class Prey implements IBoid { public var position :Vector3D; public var velocity :Vector3D; public var mass :Number; public var steering :SteeringManager; public function Prey(posX :Number, posY :Number, totalMass :Number) { position = new Vector3D(posX, posY); velocity = new Vector3D(-1, -2); mass = totalMass; steering = new SteeringManager(this); x = position.x; y = position.y; } public function update():void { velocity.normalize(); velocity.scaleBy(MAX_VELOCITY); velocity.scaleBy(1 / mass); truncate(velocity, MAX_VELOCITY); position = position.add(velocity); x = position.x; y = position.y; } // , IBoid. public function getVelocity() :Vector3D { return velocity; } public function getMaxVelocity() :Number { return 3; } public function getPosition() :Vector3D { return position; } public function getMass() :Number { return mass; } }
update()
マネージャーも更新できるように、それに応じてメソッドを変更する必要があります。
public function update():void { // , prey ... steering.wander(); // , prey. // , // "". steering.update(); // , // "". x = position.x; y = position.y; }
update()
マネージャーが呼び出される前にすべてのメソッドの呼び出しが実行されるため、すべての動作を同時に使用できます。これにより、蓄積されたすべての制御力がホスト速度ベクトルに適用されます。
以下のコードは
update()
Prey メソッドの異なるバージョンを示していますが、今回はマップ上のポイントを探し、別のキャラクターを回避(回避)します(すべて同時に)。
public function update():void { var destination :Vector3D = getDestination(); // , var hunter :IBoid = getHunter(); // , // (!) steering.seek(destination); steering.evade(hunter); // , prey. // , // "". steering.update(); // , // "". x = position.x; y = position.y; }
例
以下の例は、いくつかの動作を組み合わせた複雑なモーションパターンを示しています。シーンには、ハンター(ハンター)とプレイ(犠牲者)の2種類のキャラクターがあります。
ハンターは、被害者が十分に近づくと被害者を追いかけます。エネルギーが残っている間、彼は彼女を追いかけます(スタミナ)。エネルギーが終了すると、追跡は停止し、ハンターはエネルギーレベルを回復するまでさまよい始めます。Hunterクラスの
メソッドは
update()
次のとおりです。
public function update():void { if (resting && stamina++ >= MAX_STAMINA) { resting = false; } if (prey != null && !resting) { steering.pursuit(prey); stamina -= 2; if (stamina <= 0) { prey = null; resting = true; } } else { steering.wander(); prey = getClosestPrey(position); } steering.update(); x = position.x; y = position.y; }
犠牲者は無制限にさまよう。ハンターが近づきすぎると、彼女は彼を避けます。マウスカーソルが近くにあり、近くにハンターがいない場合、被害者はカーソルを使用する傾向があります。Prey
メソッドは次のようになり
update()
ます。
public function update():void { var distance :Number = Vector3D.distance(position, Game.mouse); hunter = getHunterWithinRange(position); if (hunter != null) { steering.evade(hunter); } if (distance <= 300 && hunter == null) { steering.seek(Game.mouse, 30); } else if(hunter == null){ steering.wander(); } steering.update(); x = position.x; y = position.y; }
おわりに
モーションマネージャは、複数のステアリング動作を同時に管理するのに非常に役立ちます。このような動作の組み合わせにより、ゲームエンティティが1つのオブジェクトを目指すと同時に別のオブジェクトを回避できる非常に複雑なモーションパターンを作成できます。
この部分に実装されたマネージャーシステムが気に入って、ゲームで使用することを願っています。
記事の第2部はこちらです。