
こんにちは、私の名前はDmitryです。私はプログラマーです。 戦術ゲームのプロジェクトの船の動きのコンポーネントをリアルタイムでリファクタリングしました。プレイヤーは自分の宇宙艦隊を組み立てて戦闘に導くことができます。 動作コンポーネントは、アルファ版のリリースから開発の開始まで、すでに3回書き換えられています。 アーキテクチャとネットワークの両方で、多くのレーキが収集されました。 このすべての経験をまとめて、ナビゲーションボリューム、動きコンポーネント、AIController、ポーンについて説明しようとします。
目的:宇宙船を飛行機に乗せるプロセスを実装する。タスク条件:
- 船には最大速度、旋回速度、加速速度があります。 これらのパラメーターは、船のダイナミクスを設定します。
- 障害物を自動的に検索し、安全なパスを確立するには、Navigtaion Volumeを使用する必要があります。
- ネットワークを介した位置座標の一定の同期があってはなりません。
- 異なる現在の速度状態から移動を開始できます。
- すべてがアンリアルエンジン4アーキテクチャにネイティブである必要があります。
プロセスアーキテクチャ
タスクを2つの段階に分けます。最初の段階は最適なパスの検索、2番目の段階はカーソルの下の終点への移動です。
最初のタスク。 最適な方法を検索する
Unrealエンジン4で最適なパスを見つけるプロセスの条件とアーキテクチャを考慮してください
。UShipMovementComponentは、
UPawnMovementComponentを継承する移動コンポーネントです。 最終ユニットである船が
APawnの相続人と
なります。
次に、
UPawnMovementComponent -
UNavMovementComponentの子孫で
あり 、構造
FNavPropertiesに追加されます-これらは、この
APawnを説明するナビゲーションパラメーターであり、
AIControllerがパスの検索時に使用します。
船が位置するレベル、静的オブジェクト、および船にまたがるナビゲーションボリュームがあるとします。 マップ上のある地点から別の地点に出荷します。これがUE4内で発生することです。

1)
APawnは 、自身の内部で
ShipAIControllerを検出し(この場合は1つのメソッドを持つ
AIControllerの子孫です )、作成したパス検索メソッドを呼び出します。
2)このメソッド内では、まずナビゲーションシステムのリクエストを準備し、次にリクエストを送信して、移動のコントロールポイントを取得します。
TArray<FVector> AShipAIController::SearchPath(const FVector& location) { FPathFindingQuery Query; const bool bValidQuery = PreparePathfinding(Query, location, NULL); UNavigationSystem* NavSys = UNavigationSystem::GetCurrent(GetWorld()); FPathFindingResult PathResult; TArray<FVector> Result; if(NavSys) { PathResult = NavSys->FindPathSync(Query); if(PathResult.Result != ENavigationQueryResult::Error) { if(PathResult.IsSuccessful() && PathResult.Path.IsValid()) { for(FNavPathPoint point : PathResult.Path->GetPathPoints()) { Result.Add(point.Location); } } } else { DumpToLog("Pathfinding failed.", true, true, FColor::Red); } } else { DumpToLog("Can't find navigation system.", true, true, FColor::Red); } return Result; }
3)これらのポイントは、
APawnのリストによって、私たちにとって便利な形式(
FVector )で返されます。 さらに、移動プロセスが開始されます。
つまり、
APawnには
ShipAIControllerがあり 、
PreparePathfinding()の呼び出し時に
APawnを呼び出して
UShipMovementComponentを受け取ります。その内部で
FNavPropertiesを見つけ、ナビゲーションシステムに渡してパスを見つけます。
2番目のタスク。 終点への移動。
したがって、移動の制御点のリストを返しました。 最初のポイントは常に現在の位置であり、最後のポイントは目的地です。 私たちの場合、これはカーソルでクリックして船を送った場所です。
少し余談をして、ネットワークでの作業をどのように構築するかについて説明する価値があります。 それをステップに分割し、それぞれを記述します。
1)移動開始メソッドを呼び出す
-AShip :: CommandMoveTo() :
UCLASS() class STARFALL_API AShip : public APawn, public ITeamInterface { ... UFUNCTION(BlueprintCallable, Server, Reliable, WithValidation, Category = "Ship") void CommandMoveTo(const FVector& location); void CommandMoveTo_Implementation(const FVector& location); bool CommandMoveTo_Validate(const FVector& location); ... }
注意してください-クライアント側では、すべてのPawn'ovには
AIControllerがなく、サーバー上にのみ存在します。 したがって、クライアントが船舶を新しい場所に送信するメソッドを呼び出すとき、サーバーですべての誤計算を実行する必要があります。 言い換えれば、サーバーは各船の経路を見つけるのに忙しくなります。 ナビゲーションシステム
で動作するのは
AIControllerであるためです。
2)
CommandMoveTo()メソッド内で制御点のリストを見つけ
たら 、次を呼び出して船の動きを開始します。 このメソッドは、すべてのクライアントで呼び出す必要があります。
UCLASS() class STARFALL_API AShip : public APawn, public ITeamInterface { ... UFUNCTION(BlueprintCallable, Reliable, NetMulticast, Category = "Ship") void StartNavMoveFrom(const FVector& location); virtual void StartNavMoveFrom_Implementation(const FVector& location); ... }
この方法では、制御点を持たないクライアントは、制御点のリストで彼に渡された最初の座標を含め、「エンジンを起動して」移動を開始します。 この時点で、サーバー上で、タイマーを介して、パスの残りの中間点と終点の送信を開始します。
void AShip::CommandMoveTo(const FVector& location) { ... GetWorldTimerManager().SetTimer(timerHandler, FTimerDelegate::CreateUObject(this, &AShip::SendNextPathPoint), 0.1f, true); ... }
UCLASS() class STARFALL_API AShip : public APawn, public ITeamInterface { ... FTimerHandle timerHandler; UFUNCTION(BlueprintCallable, Reliable, NetMulticast, Category = "Ship") void SendPathPoint(const FVector& location); virtual void SendPathPoint_Implementation(const FVector& location); ... }
クライアント側では、船が加速して経路の最初の制御ポイントに移動し始めている間に、徐々に残りを受け取り、それらをアレイに配置します。 これにより、ネットワークをアンロードし、データの送信を時間内にストレッチして、ネットワークに負荷を分散できます。
余談で終わり、問題の本質に戻る。 現在のタスクは、最も近いコントロールポイントに向かって飛行を開始することです。 条件の下で、私たちの船には回転速度、加速、最大速度があることに注意してください。 したがって、新しい目的地への出発時には、船は、例えば、全速力で飛行したり、静止したり、加速したり、旋回中であったりします。 したがって、船は現在の速度特性と目的地に基づいて異なる動作をする必要があります。 船の3つの主要な動作ラインを特定しました。

- 加速を少なくとも最大速度に制限することなく目的地に飛ぶことができ、同時にターンに収まりその場所に到着するのに十分な回転速度が得られます。
- 私たちの速度を考えると、私たちはあまりに速く飛ぶので、低速で目的地まで飛行しようとし、船の船首が明確にその方向を指し示したら、最大速度まで加速しようとします
- 愚かな方向を変えて直線で飛行するよりも時間がかかる場合は、簡単な方法で進めます。
そのため、ポイントへの移動を開始する前に、飛行する速度パラメーターを決定する必要があります。 これを行うために、フライトシミュレーションメソッドを実装します。 誰かが非常に興味があるなら、私は彼女のコードをここに持ってきません-書いて、私が言います。 その本質は単純
です 。現在の
DeltaTimeを使用して、常に位置のベクトルを移動し、視線の方向を前方に向けて、船の回転をシミュレートします。 これらは、
FRotatorを使用したベクターでの最も単純な操作
です 。 少し想像すると、これを簡単に実現できます。
言及する価値のある唯一のポイントは、船の回転の各反復で、どれだけすでにそれを回したかを覚えておく必要があるということです。 180度を超える場合、これは目的地の周りを旋回し始めていることを意味し、コントロールポイントに到達するために次の速度パラメーターを試す必要があります。 当然、最初にフルスピードで飛行し、次に低速で飛行しようとします(現在は中速で作業しています)。これらのオプションがいずれも当てはまらない場合、船はただ旋回して飛行する必要があります。
AIControllerがクライアント上になく、
UShipMovementComponentが別の役割を果たしているため、状況と移動プロセスを評価するロジック全体を
AShipに実装する必要があるという事実に注意を喚起したいと思います(それについては少し低くなりました)。 したがって、船舶が独立して移動できるように、座標をサーバーと常に同期させる必要はありません(これは必要ありません)
。AShip内にモーション制御ロジックを実装する必要があります。
したがって、現在、これらすべてについて最も重要なことは、
UShipMovementComponentムーブメントのコンポーネントです。 これらのタイプのコンポーネントがモーターであることを認識することは価値があります。 それらの機能は、ガスを前方に送り、オブジェクトを回転させることです。 彼らは、オブジェクトがどのロジックを移動すべきかを考えず、オブジェクトがどのような状態にあるかを考えません。 オブジェクトの実際の動きにのみ責任を負います。 燃料の詰め込みと宇宙への移動に。
UMovementComponentとその子孫を使用するロジックは次のとおりです。この
Tick()では、コンポーネントのパラメーター(速度、最大速度、回転速度)に関連するすべての数学計算を行い、パラメーター
UMovementComponent :: Velocityを次の値に設定します。このティックへの船舶のシフトに関連して、
UMovementComponent :: MoveUpdatedComponent()を呼び出します。これは、船舶のシフトとその回転が行われる場所です。
void UShipMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if(!PawnOwner || !UpdatedComponent || ShouldSkipUpdate(DeltaTime)) { return; } if (CheckState(EShipMovementState::Accelerating)) { if (CurrentSpeed < CurrentMaxSpeed) { CurrentSpeed += Acceleration; AccelerationPath += CurrentSpeed*DeltaTime; } else { CurrentSpeed = CurrentMaxSpeed; RemoveState(EShipMovementState::Accelerating); } } else if (CheckState(EShipMovementState::Braking)) { if (CurrentSpeed > 0.0f) { CurrentSpeed -= Acceleration; DeaccelerationPath += CurrentSpeed*DeltaTime; } else { CurrentSpeed = 0.0f; CurrentMaxSpeed = MaxSpeed; RemoveState(EShipMovementState::Braking); RemoveState(EShipMovementState::Moving); } } else if (CheckState(EShipMovementState::SpeedDecreasing)) { if (CurrentSpeed > CurrentMaxSpeed) { CurrentSpeed -= Acceleration; DeaccelerationPath += CurrentSpeed*DeltaTime; } else { CurrentSpeed = CurrentMaxSpeed; RemoveState(EShipMovementState::SpeedDecreasing); } } if (CheckState(EShipMovementState::Moving) || CheckState(EShipMovementState::Turning)) { MoveForwardWithCurrentSpeed(DeltaTime); } } ... void UShipMovementComponent::MoveForwardWithCurrentSpeed(float DeltaTime) { Velocity = UpdatedComponent->GetForwardVector() * CurrentSpeed * DeltaTime; MoveUpdatedComponent(Velocity, AcceptedRotator, false); UpdateComponentVelocity(); } ...
ここに表示される条件について2つの単語を言います。 それらは、異なる動きのプロセスを組み合わせるために必要です。 たとえば、速度を落として(機動のために中速に切り替える必要があるため)、新しい目的地に向かうことができます。 モーションコンポーネントでは、速度を使用した作業を評価するためにのみ使用します。速度の設定や速度の低下などを継続する必要がありますか。 移動の状態から別の状態への移行に関連するすべてのロジックは、
AShipで発生し
ます 。たとえば、最大速度で移動し、宛先を変更します。それを実現するには、速度を中にリセットする必要があります。
AcceptedRotatorについての最後の2ペニー。 これがこのチークの船の番です。
AShipティックでは、
UShipMovementComponentの次のメソッドを呼び出します。
bool UShipMovementComponent::AcceptTurnToRotator(const FRotator& RotateTo) { if(FMath::Abs(RotateTo.Yaw - UpdatedComponent->GetComponentRotation().Yaw) < 0.1f) { return true; } FRotator tmpRot = FMath::RInterpConstantTo(UpdatedComponent->GetComponentRotation(), RotateTo, GetWorld()->GetDeltaSeconds(), AngularSpeed); AcceptedRotator = tmpRot; return false; }
RotateTo =(GoalLocation-ShipLocation).Rotation() -つまり これはローテーターであり、目的地を見るために船の回転がどのような値である必要があるかを示します。 そして、この方法では、船が目的地を見ているかどうかを評価しますか? 彼が見たら、そのような結果を返すので、振り向く必要はありません。 そして、評価ロジックの
AShipはEShipMovementState :: Turning-をリセットし、
UShipMovementComponentはスピンしなくなります。 それ以外の場合、船の回転を取得し、
DeltaTimeと船の回転速度を考慮して解釈します。 次に、
UMovementComponent :: MoveUpdatedComponentを呼び出すときに、この回転を現在のティックに適用します。
見込み
この
UShipMovementComponentの生まれ変わりは、プロトタイプ段階で遭遇したすべての問題を考慮しているように
思えます 。 また、このバージョンは拡張可能であることが判明し、現在ではさらに開発する機会があります。 たとえば、船を回すプロセス:ちょうど船を回すと、まるでそれが回転するロッドに張られているかのように、退屈に見えます。 ただし、回転方向にわずかに鼻のロールを追加すると、このプロセスがより魅力的になります。
また、船の中間位置の同期が最小限になりました。 宛先に到達するとすぐに、データをサーバーと同期します。 これまでのところ、サーバーとクライアントの最終的な位置の違いはごくわずかですが、もしそれが大きくなれば、宇宙空間で船をけいれんしたり「ジャンプ」させずに、この同期をスムーズにクランクする方法について多くのアイデアがあります。 しかし、私はおそらくこのことについてもう一度お話します。