- Gameplay Ability System コードリーディング ① WaitOverlap - You are done!
- Gameplay Ability System コードリーディング ② SpawnActor - You are done!
- Gameplay Ability System コードリーディング ③ WaitTargetData - You are done!
- Gameplay Ability System コードリーディング ④ WaitTargetData - GameplayAbilityTargetActor (及び Radius) - You are done!
- Gameplay Ability System コードリーディング ⑤ WaitTargetData - Radius の利用 - You are done!
- Gameplay Ability System コードリーディング ⑥ WaitTargetData - Confirmation Type - You are done!
- Gameplay Ability System コードリーディング ⑦ WaitTargetData - Confirmation Type (Custom / CustomMulti) - You are done!
上記の続きになります。
ここまでは簡単そうな Radius (AGameplayAbilityTargetActor_Radius) を見つつ、ConfirmationType などを見てきました。
今回からは AGameplayAbilityTargetActor_Trace から続くトレース関連を見ていきます。

SingleLineTrace
SingleLineTrace |
Radius |
|---|---|
![]() |
![]() |
Radius は Radius プロパティだけに対して SingleLineTrace は Max Range, Trace Profile, Trace Affects Aim Pitch の 3 つが用意されています。他は共通しています。
共通する部分はこれらの基底である AGameplayAbilityTargetActor に定義されているものです。
ヘッダ
AGameplayAbilityTargetActor_SingleLineTrace が AGameplayAbilityTargetActor_Trace を継承しているため、SingleLineTrace は以下のように小さいです。
UCLASS(Blueprintable) class GAMEPLAYABILITIES_API AGameplayAbilityTargetActor_SingleLineTrace : public AGameplayAbilityTargetActor_Trace { GENERATED_UCLASS_BODY() protected: virtual FHitResult PerformTrace(AActor* InSourceActor) override; };
親の Trace 側は以下のようになっています。
/** Intermediate base class for all line-trace type targeting actors. */ UCLASS(Abstract, Blueprintable, notplaceable, config=Game) class GAMEPLAYABILITIES_API AGameplayAbilityTargetActor_Trace : public AGameplayAbilityTargetActor { GENERATED_UCLASS_BODY() public: virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; /** Traces as normal, but will manually filter all hit actors */ static void LineTraceWithFilter(FHitResult& OutHitResult, const UWorld* World, const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, FName ProfileName, const FCollisionQueryParams Params); /** Sweeps as normal, but will manually filter all hit actors */ static void SweepWithFilter(FHitResult& OutHitResult, const UWorld* World, const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, const FQuat& Rotation, const FCollisionShape CollisionShape, FName ProfileName, const FCollisionQueryParams Params); void AimWithPlayerController(const AActor* InSourceActor, FCollisionQueryParams Params, const FVector& TraceStart, FVector& OutTraceEnd, bool bIgnorePitch = false) const; static bool ClipCameraRayToAbilityRange(FVector CameraLocation, FVector CameraDirection, FVector AbilityCenter, float AbilityRange, FVector& ClippedPosition); virtual void StartTargeting(UGameplayAbility* Ability) override; virtual void ConfirmTargetingAndContinue() override; virtual void Tick(float DeltaSeconds) override; UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = Trace) float MaxRange; UPROPERTY(BlueprintReadWrite, EditAnywhere, config, meta = (ExposeOnSpawn = true), Category = Trace) FCollisionProfileName TraceProfile; // Does the trace affect the aiming pitch UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = Trace) bool bTraceAffectsAimPitch; protected: virtual FHitResult PerformTrace(AActor* InSourceActor) PURE_VIRTUAL(AGameplayAbilityTargetActor_Trace, return FHitResult();); FGameplayAbilityTargetDataHandle MakeTargetData(const FHitResult& HitResult) const; TWeakObjectPtr<AGameplayAbilityWorldReticle> ReticleActor; };
以下に入力ピンになっているものを抜き出しましたが、先程確認した 3 つが定義されていることがわかります。つまり、これらは SingleLineTrace 固有のものではなく、Trace 全般で共通して利用するプロパティのようです。
UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = Trace) float MaxRange; UPROPERTY(BlueprintReadWrite, EditAnywhere, config, meta = (ExposeOnSpawn = true), Category = Trace) FCollisionProfileName TraceProfile; // Does the trace affect the aiming pitch UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = Trace) bool bTraceAffectsAimPitch;
StartTargeting
Radius でも確認しましたが、StartTargeting は、これら AGameplayAbilityTargetActor を利用する WaitTargetData (AbilityTask_WaitTargetData) から呼び出されます。
AGameplayAbilityTargetActor が WaitTargetData の初期処理で Spawn され、StartTargeting を呼び出してこの Actor の開始が行われるということです。
void AGameplayAbilityTargetActor_Trace::StartTargeting(UGameplayAbility* InAbility) { Super::StartTargeting(InAbility); SourceActor = InAbility->GetCurrentActorInfo()->AvatarActor.Get(); if (ReticleClass) { // 略 // ReticleActor = ~ } }
Reticle に関してはここでは割愛しますので、SourceActor として Ability が紐付けられている Actor (例えばプレイヤーキャラクター)を入れています。
EndPlay
AActor の EndPlay の override です。
void AGameplayAbilityTargetActor_Trace::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (ReticleActor.IsValid()) { ReticleActor.Get()->Destroy(); } Super::EndPlay(EndPlayReason); }
StartTargeting で ReticleActor に値が入れられた場合にそれを Destroy しています。
Tick
AGameplayAbilityTargetActor は Actor なので Tick が動作します。
AGameplayAbilityTargetActor_Trace::AGameplayAbilityTargetActor_Trace(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryActorTick.bCanEverTick = true; PrimaryActorTick.TickGroup = TG_PostUpdateWork; MaxRange = 999999.0f; bTraceAffectsAimPitch = true; }
コンストラクタを見てわかる通り Tick の設定もされています。
void AGameplayAbilityTargetActor_Trace::Tick(float DeltaSeconds) { // very temp - do a mostly hardcoded trace from the source actor if (SourceActor && SourceActor->GetLocalRole() != ENetRole::ROLE_SimulatedProxy) { FHitResult HitResult = PerformTrace(SourceActor); FVector EndPoint = HitResult.Component.IsValid() ? HitResult.ImpactPoint : HitResult.TraceEnd; #if ENABLE_DRAW_DEBUG if (bDebug) { DrawDebugLine(GetWorld(), SourceActor->GetActorLocation(), EndPoint, FColor::Green, false); DrawDebugSphere(GetWorld(), EndPoint, 16, 10, FColor::Green, false); } #endif // ENABLE_DRAW_DEBUG SetActorLocationAndRotation(EndPoint, SourceActor->GetActorRotation()); } }
トレースの概要 | Unreal Engine ドキュメント
PerformTrace は SingleLineTrace 側での実装になっていますのでここではスキップします。
ただ、FHitResult を返しており、Hit があるかないかで ImpactPoint か TraceEnd を取得しており、それに応じて自身(つまり AGameplayAbilityTargetActor_SingleLineTrace) の SetActorLocationAndRotation を呼び出しています。
継続して Trace をするためにこの Actor 自身も移動しています。(どうして EndPoint に移動するのか、、は分かりません 😥)
AGameplayAbilityTargetActor_SingleLineTrace::PerformTrace
Tick で呼び出される関数と分かりましたので引き続き PerformTrace の詳細を見ていきます。
FHitResult AGameplayAbilityTargetActor_SingleLineTrace::PerformTrace(AActor* InSourceActor) { bool bTraceComplex = false; TArray<AActor*> ActorsToIgnore; ActorsToIgnore.Add(InSourceActor); FCollisionQueryParams Params(SCENE_QUERY_STAT(AGameplayAbilityTargetActor_SingleLineTrace), bTraceComplex); Params.bReturnPhysicalMaterial = true; Params.AddIgnoredActors(ActorsToIgnore); FVector TraceStart = StartLocation.GetTargetingTransform().GetLocation(); // InSourceActor->GetActorLocation(); FVector TraceEnd; AimWithPlayerController(InSourceActor, Params, TraceStart, TraceEnd); // Effective on server and launching client only // ------------------------------------------------------ FHitResult ReturnHitResult; LineTraceWithFilter(ReturnHitResult, InSourceActor->GetWorld(), Filter, TraceStart, TraceEnd, TraceProfile.Name, Params); //Default to end of trace line if we don't hit anything. if (!ReturnHitResult.bBlockingHit) { ReturnHitResult.Location = TraceEnd; } if (AGameplayAbilityWorldReticle* LocalReticleActor = ReticleActor.Get()) { // 略 } #if ENABLE_DRAW_DEBUG if (bDebug) { // 略 } #endif // ENABLE_DRAW_DEBUG return ReturnHitResult; }
FCollisionQueryParams Paramsの設定で、SourceActorつまりこの Ability のオーナーが除外されていますTraceStartはStartLocation(FGameplayAbilityTargetingLocationInfo) のGetTargetingTransform関数から取得していますTraceEndはAGameplayAbilityTargetActor_Trace::AimWithPlayerControllerで計算されています
ここまでで Trace の準備ができたため、AGameplayAbilityTargetActor_Trace::LineTraceWithFilter に上記で準備した値が渡され、ReturnHitResult に値が結果が入ります。
その結果を受けて if (!ReturnHitResult.bBlockingHit) で分かる通り、ブロッキングでヒットした場合以外は TraceEnd が ReturnHitResult.Location に入れられます。 (Overlap の場合はヒットしていないと同じ扱いということですかね)
LineTraceWithFilter
AGameplayAbilityTargetActor_Trace の関数なので、SingleLineTrace 固有ではありません。
void AGameplayAbilityTargetActor_Trace::LineTraceWithFilter(FHitResult& OutHitResult, const UWorld* World, const FGameplayTargetDataFilterHandle FilterHandle, const FVector& Start, const FVector& End, FName ProfileName, const FCollisionQueryParams Params) { check(World); TArray<FHitResult> HitResults; World->LineTraceMultiByProfile(HitResults, Start, End, ProfileName, Params); OutHitResult.TraceStart = Start; OutHitResult.TraceEnd = End; for (int32 HitIdx = 0; HitIdx < HitResults.Num(); ++HitIdx) { const FHitResult& Hit = HitResults[HitIdx]; if (!Hit.HitObjectHandle.IsValid() || FilterHandle.FilterPassesForActor(Hit.HitObjectHandle.FetchActor())) { OutHitResult = Hit; OutHitResult.bBlockingHit = true; // treat it as a blocking hit return; } } }
入力ピンで受け取っていた ProfileName を使って LineTraceMultiByProfile を実行しています。
ここで取得できた TArray<FHitResult> から単一の OutHitResult を返すわけですが、for ループで回して条件の合致をチェックして最初に合致したものの FHitResult の情報を使っているようです。
if (!Hit.HitObjectHandle.IsValid() || FilterHandle.FilterPassesForActor(Hit.HitObjectHandle.FetchActor())) に関してですが、後者の Filter に関しては、入力ピンの Filter を使っている部分は Radius でも同様の判定があったので理解できるのですが、!Hit.HitObjectHandle.IsValid() に関しては私の知識ではピンときませんでした。
HitObjectHandle が無い Hit というものがどういうケースをさしているのかが分かりませんが、その場合も有効なブロッキングヒットとして扱うようになっているようです。
AimWithPlayerController
こちらも AGameplayAbilityTargetActor_Trace の関数です。前述の通り、この関数の結果として利用するのは OutTraceEnd です。
void AGameplayAbilityTargetActor_Trace::AimWithPlayerController(const AActor* InSourceActor, FCollisionQueryParams Params, const FVector& TraceStart, FVector& OutTraceEnd, bool bIgnorePitch) const { if (!OwningAbility) // Server and launching client only { return; } APlayerController* PC = OwningAbility->GetCurrentActorInfo()->PlayerController.Get(); check(PC); FVector ViewStart; FRotator ViewRot; PC->GetPlayerViewPoint(ViewStart, ViewRot); const FVector ViewDir = ViewRot.Vector(); FVector ViewEnd = ViewStart + (ViewDir * MaxRange); ClipCameraRayToAbilityRange(ViewStart, ViewDir, TraceStart, MaxRange, ViewEnd); FHitResult HitResult; LineTraceWithFilter(HitResult, InSourceActor->GetWorld(), Filter, ViewStart, ViewEnd, TraceProfile.Name, Params); const bool bUseTraceResult = HitResult.bBlockingHit && (FVector::DistSquared(TraceStart, HitResult.Location) <= (MaxRange * MaxRange)); const FVector AdjustedEnd = (bUseTraceResult) ? HitResult.Location : ViewEnd; FVector AdjustedAimDir = (AdjustedEnd - TraceStart).GetSafeNormal(); if (AdjustedAimDir.IsZero()) { AdjustedAimDir = ViewDir; } if (!bTraceAffectsAimPitch && bUseTraceResult) { FVector OriginalAimDir = (ViewEnd - TraceStart).GetSafeNormal(); if (!OriginalAimDir.IsZero()) { // Convert to angles and use original pitch const FRotator OriginalAimRot = OriginalAimDir.Rotation(); FRotator AdjustedAimRot = AdjustedAimDir.Rotation(); AdjustedAimRot.Pitch = OriginalAimRot.Pitch; AdjustedAimDir = AdjustedAimRot.Vector(); } } OutTraceEnd = TraceStart + (AdjustedAimDir * MaxRange); }
まず PC->GetPlayerViewPoint(ViewStart, ViewRot); です。
APlayerController::GetPlayerViewPoint | Unreal Engine Documentation
GASDocumentation の場合は TPS なので、キャラクターの後ろにあるカメラのポイントになります。
FVector ViewEnd = ViewStart + (ViewDir * MaxRange); で ViewEnd を出していますが、MaxRange が入力ピンから受け取っているものなので、ここだけみると ViewEnd はカメラからの距離で測っているようです。
ClipCameraRayToAbilityRange
ClipCameraRayToAbilityRange 関数が呼び出されます、ここで ViewEnd の位置を計算し直しているようです。
bool AGameplayAbilityTargetActor_Trace::ClipCameraRayToAbilityRange(FVector CameraLocation, FVector CameraDirection, FVector AbilityCenter, float AbilityRange, FVector& ClippedPosition) { FVector CameraToCenter = AbilityCenter - CameraLocation; float DotToCenter = FVector::DotProduct(CameraToCenter, CameraDirection); if (DotToCenter >= 0) //If this fails, we're pointed away from the center, but we might be inside the sphere and able to find a good exit point. { float DistanceSquared = CameraToCenter.SizeSquared() - (DotToCenter * DotToCenter); float RadiusSquared = (AbilityRange * AbilityRange); if (DistanceSquared <= RadiusSquared) { float DistanceFromCamera = FMath::Sqrt(RadiusSquared - DistanceSquared); float DistanceAlongRay = DotToCenter + DistanceFromCamera; //Subtracting instead of adding will get the other intersection point ClippedPosition = CameraLocation + (DistanceAlongRay * CameraDirection); //Cam aim point clipped to range sphere return true; } } return false; }
変数名が変わっているので以下に対応表を書きます。
| 関数の外 | 関数内 |
|---|---|
ViewStart |
CameraLocation |
ViewDir |
CameraDirection |
TraceStart |
AbilityCenter |
MaxRange |
AbilityRange |
ViewEnd |
ClippedPosition |
3次元のベクトルをスクショで表示しようというのが無理がある上に全て位置ベクトルでの表示で余計にややこしいのですが
FVector CameraToCenter = AbilityCenter - CameraLocation; float DotToCenter = FVector::DotProduct(CameraToCenter, CameraDirection);
この 2 行に関してのみ図示してみます。この場合、AbilityCenter つまり TraceStart はキャラクター前方の Sphere の位置を指定していると考えてください。

まず赤で表される CameraToCenter ですが、AbilityCenter と CameraLocation のベクトルの差です。実際には3次元なので見た目の長さや角度に違和感はありますが、以下のような感じになると思います。(ホント、もっと良い見せ方ないのか。。)

続いて DotToCenter ですが、CameraToCenter と CameraDirection の内積です。CameraDirection は大きさ 1 なので CameraToCenter を CameraDirection の方向にした場合の大きさです。
図では黒線で表していますが、DotToCenter は長さなので、CameraDirection の方向に図示しています。例によって 3次元だとよくわからん感はあるのですが、赤線が CameraDirection 方向に射影されたら黒線ぐらいの長さになるというのはなんとなく分かるような分からんような。
続いて if (DotToCenter >= 0) で判定していますが、これが負になるということは、カメラの向きと、カメラ - トレーススタートの向きが 90 度より大きいということなのでほぼ視界外れているか端の方にいるということになりそうなのでここでは false としているのでしょうか。(コメントの意図はよく分かりませんでした 😅)
float DistanceSquared = CameraToCenter.SizeSquared() - (DotToCenter * DotToCenter); float RadiusSquared = (AbilityRange * AbilityRange); // 長さの比較 if (DistanceSquared <= RadiusSquared) { float DistanceFromCamera = FMath::Sqrt(RadiusSquared - DistanceSquared); float DistanceAlongRay = DotToCenter + DistanceFromCamera; //Subtracting instead of adding will get the other intersection point ClippedPosition = CameraLocation + (DistanceAlongRay * CameraDirection); //Cam aim point clipped to range sphere return true; }
まずは長さの比較です。ベクトルが混ざっているので全体的に 2 乗されていますが最終的に長さを比較しています。
まず AbilityRange ですが、これは入力の MaxRange ですので Trace する範囲(ここでは SingleLine なので単純に線の長さ)です。
これと比較するのは CameraToCenter と DotToCenter の長さを引いたものです。DotToCenter は前述の通り、単に CameraToCenter を CameraDirection 方向にしただけです。
自分でもふわっとした理解になってしまうのですが、DistanceSquared <= RadiusSquared は、まず DistanceSquared は「カメラからトレース開始位置の距離」と「それをカメラの方向を考慮した距離」の差なので、もしカメラがトレース開始位置とは異なる方向を向いていればいる程「その差は大きく」なります。
そして最終的に ClippedPosition は CameraLocation に対しての位置を計算します。つまり、上記の「差」が「トレース範囲」よりも大きくなってしまうと「カメラとトレース開始位置の間のみを対象とする」ことになりかねないということだと思います。(実際、この ClippedPosition は ViewEnd として LineTraceWithFilter のパラメータとして使われます)
DistanceFromCamera がカメラの方向も考慮した上でのトレースする距離を表し、DistanceAlongRay でそれをカメラ方向を考慮したトレース開始位置に足し合わせることで、カメラからの総トレース距離を計算しています。
私個人としては、SingleLineTrace といえば、「キャラクターの前方からトレースして」みたいなことを実装することがほとんどなので、こういうコードを見るのはとても新鮮でした。
再び AimWithPlayerController に戻る
ClipCameraRayToAbilityRange が長くなったので以下に以降の処理を貼り直します。(全部ではありません)
ClipCameraRayToAbilityRange(ViewStart, ViewDir, TraceStart, MaxRange, ViewEnd); FHitResult HitResult; LineTraceWithFilter(HitResult, InSourceActor->GetWorld(), Filter, ViewStart, ViewEnd, TraceProfile.Name, Params); const bool bUseTraceResult = HitResult.bBlockingHit && (FVector::DistSquared(TraceStart, HitResult.Location) <= (MaxRange * MaxRange)); const FVector AdjustedEnd = (bUseTraceResult) ? HitResult.Location : ViewEnd; FVector AdjustedAimDir = (AdjustedEnd - TraceStart).GetSafeNormal(); if (AdjustedAimDir.IsZero()) { AdjustedAimDir = ViewDir; }
ClipCameraRayToAbilityRange で ViewEnd が得られましたので、それを使って LineTraceWithFilter を行っています。ViewStart はカメラの位置です。これを行うために ClipCameraRayToAbilityRange でカメラからトレースする範囲を計算していたんですね。
その結果を受けて、BlockingHit かつ (FVector::DistSquared(TraceStart, HitResult.Location) <= (MaxRange * MaxRange)) でトレース開始位置(カメラ位置ではなく入力ピンから受け取ったトレース開始位置)からヒットした場所の距離と、トレース範囲の距離を比較し、有効範囲であれば bUseTraceResult = true としています。トレース開始位置からではなく、カメラからのトレースなので意図しない場所でヒットしないかをチェックしているのだと思います。
AdjustedEnd には、先程の bUseTraceResult に応じて、ヒットした場所か ViewEnd (ClipCameraRayToAbilityRange で計算したもの) を入れています。
AdjustedAimDir は AdjustedEnd と TraceStart の差の向きのみを取得しています。
そして、あまりに差が小さい場合には ViewDir (カメラ向き) を代わりに利用しています。
AimDir ということは、狙う方向的なことだと思うので、カメラからトレースしてヒットしたベクトルと、トレース開始位置のベクトルの差というのはなるほどという感じです。
AimWithPlayerController の最後のブロックです。
まず、if ブロックを通過しない場合は、先程の AdjustedAimDir を使ってトレース開始位置からトレース距離分 AdjustedAimDir を伸ばしたベクトルを OutTraceEnd としています。
if (!bTraceAffectsAimPitch && bUseTraceResult) { FVector OriginalAimDir = (ViewEnd - TraceStart).GetSafeNormal(); if (!OriginalAimDir.IsZero()) { // Convert to angles and use original pitch const FRotator OriginalAimRot = OriginalAimDir.Rotation(); FRotator AdjustedAimRot = AdjustedAimDir.Rotation(); AdjustedAimRot.Pitch = OriginalAimRot.Pitch; AdjustedAimDir = AdjustedAimRot.Vector(); } } OutTraceEnd = TraceStart + (AdjustedAimDir * MaxRange);
bTraceAffectsAimPitch は入力ピンで受け取る値です。Pitch に関してはここまでの結果を使わないとうことでしょうか。
まず、ViewEnd (ClipCameraRayToAbilityRange で取得) と TraceStart (トレース開始位置 ) の差の向きを OriginalAimDir としています。
それが有効な場合には、Pitch のみ AdjustedAimDir から差し替えているようです。(と書いていますが、Vector -> Rotation -> Vector の変換ってよくわかってない。。)
結局 AimWithPlayerController はなんだったのか
結果として受け取るのは OutTraceEnd 、つまり外側の値としては TraceEnd、SingleLineTrace のトレース範囲の端です。
そのために、カメラの向きを考慮したトレース範囲を計算し、それを持って一度 LineTrace を行い、その結果としてヒットが得られればその方向にトレースの方向を調整しています。
よって、私のような素人がよくやる Trace はキャラクターの ForwardVector でみたいなものではなく、カメラの向きを考慮した上でトレースの方向を決めているという感じと個人的には理解しました。
ConfirmTargetingAndContinue
Radius のところでも見ましたが、Trace 系も同様に ConfirmTargetingAndContinue で AbilityTask_WaitTargetData の confirm を受け付けます。つまりこれが実行された時点でこの GameplayAbilityTargetActor_SingleLineTrace はデータを出力します。
Tick はあくまで TargetActor 自身の SetActorLocationAndRotation しか更新していません。
void AGameplayAbilityTargetActor_Trace::ConfirmTargetingAndContinue() { check(ShouldProduceTargetData()); if (SourceActor) { bDebug = false; FGameplayAbilityTargetDataHandle Handle = MakeTargetData(PerformTrace(SourceActor)); TargetDataReadyDelegate.Broadcast(Handle); } } FGameplayAbilityTargetDataHandle AGameplayAbilityTargetActor_Trace::MakeTargetData(const FHitResult& HitResult) const { /** Note: This will be cleaned up by the FGameplayAbilityTargetDataHandle (via an internal TSharedPtr) */ return StartLocation.MakeTargetDataHandleFromHitResult(OwningAbility, HitResult); }
Tick でも実行されていた PerformTrace を再度実行し、その時点でヒットするデータを FGameplayAbilityTargetDataHandle に詰めた上で delegate に流しています。
まとめ
簡単には、トレース開始位置、範囲、トレース対象のプロファイルを渡した上で、内部では LineTraceByProfile を行い、Confirm 時点でヒットしているデータを渡すというだけのものでした。
加えて、トレース開始位置と範囲だけを渡しているため、トレース終了位置を内部で決定しておりその処理が個人的には面白いものでした。

