Dizine dön

Distance Matching — Start & Stop

Locomotion R-001 Çizen: Alparslan Tamam

Locomotion'daki start/stop geçişlerinin nasıl çalıştığını, hangi şartlara bağlı olduğunu ve bozulduğunda nasıl teşhis edileceğini anlatır. Distance curve'ün işaret konvansiyonu, AdvanceTimeByDistanceMatching ile DistanceMatchToTarget'ın iç işleyişi, üç sessiz kurulum tuzağı ve ölçülmüş bir bozuk curve örneği.

UE 5.8 · Plugin AnimationLocomotionLibrary · Ölçümler ana karakterin unarmed seti üzerinden.

Problem

Karakter durup koşmaya başladığında iki bağımsız sistem aynı anda çalışır:

Sistem Neyi sürer Ölçülen projedeki değer
UCharacterMovementComponent (projenin alt sınıfı) Kapsülün konumunu MaxWalkSpeed = 600, MaxAcceleration = 2048
Animation Blueprint Pozu AS_Run_Start, 0.5 s

İkisi birbirinden habersizdir. Kapsül 87.9 cm’de tam hıza ulaşırken animasyon hâlâ ikinci adımını atıyorsa ayak yerde kayar.

Play rate’i hıza bölmek naif çözümdür ve karakteri gülünç hızlarda oynatır. Distance matching bunun yerine kare seçimini değiştirir.

Temel fikir

Animasyonun hangi karesinde olduğunu saatle değil, alınan yolla belirle.

Karakter 40 cm gittiyse animasyonun “40 cm gidilmiş” karesi gösterilir. Ayak nereye basıyorsa oraya basar, çünkü kare zaten mesafeye göre seçilmiştir.

Bunun için animasyonun her karesinde ne kadar yol alındığı bilinmelidir → Distance curve.

Distance curve nedir

Animasyona gömülü, isimle okunan bir float track. Her key bir soruyu cevaplar: “bu anda root ne kadar yol almıştı?”

Doğru bir curve — AS_Run_Stop

21 key, 0.667 s. Değer −166.35’ten 0’a hiç geri gitmeden yükselir:

t=0.000  ████████████████████████████████████████  -166.35   ← klip başı
t=0.067  ██████████████████████████████            -125.61
t=0.133  █████████████████████                      -88.54
t=0.200  ███████████                                -48.62
t=0.267  ██████                                     -26.86
t=0.333  ███                                        -16.27
t=0.400  ██                                          -8.72
t=0.467  █                                           -4.35
t=0.533                                              -1.72
t=0.600                                              -0.38
t=0.667                                               0.00   ← ayak yere basıyor

Aranan şekil budur: tek yönlü, tekrarsız, düzgün.

İşaret konvansiyonu

DistanceCurveModifier.h:23-26 tanımı verir:

“A negative value indicates distance remaining to a stop or pivot point. A positive value indicates distance traveled from a start point or from the beginning of the clip.”

Klip Curve Okunuşu
Start 0 → +125 ne kadar yol aldım
Stop −166 → 0 durmama ne kadar kaldı

Dikkat: ikisi de artan. Bu tesadüf değil — arama algoritması buna bağlı (bkz. Stop — DistanceMatchToTarget).

Hangi klip hangi fonksiyona gider

Karakter hareket hâlinde
          │
          ▼
     Hız durumu?
     ├─ Sabit hız  → Loop klibi  → SetPlayrateToMatchSpeed (play rate ölçekleme)
     ├─ Hızlanıyor → Start klibi → AdvanceTimeByDistanceMatching (birikimli ilerleme) ─┐
     └─ Yavaşlıyor → Stop klibi  → DistanceMatchToTarget (ikili arama) ────────────────┤
                                                                                        ▼
                                                     SetExplicitTime (Sequence Evaluator)

Neden iki farklı algoritma:

  • Stop’ta hedef bellidir. Karakter fren yapıyor, duracağı nokta hesaplanabiliyor. Soru: “32 cm kala hangi karedeydim?” → curve’de ara.
  • Start’ta hedef yoktur. Karakter ivmeleniyor, nerede biteceği belli değil. Soru: “bu karede 4 cm gittim, animasyonu ne kadar ilerleteyim?”birik.

Bu ayrım pratikte önemlidir, çünkü arıza şekilleri farklıdır.

İki yolun iç işleyişi

Start — AdvanceTimeByDistanceMatching

İkili arama yapmaz. Sabit 1/30 s’lik adımlarla curve üzerinde yürür ve mesafe biriktirir (AnimDistanceMatchingLibrary.cpp:84-152):

const float AnimationDistanceThisStep = DistanceAfterStep - CurrentDistance;
if (!FMath::IsNearlyZero(AnimationDistanceThisStep))
{
    if (AccumulatedDistance + AnimationDistanceThisStep < DistanceTraveled)
    {
        FAnimationRuntime::AdvanceTime(bAllowLooping, StepTime, NewTime, SequenceLength);
        AccumulatedDistance += AnimationDistanceThisStep;   // negatif de olabilir
    }
    ...
Bu karede alınan yol: DistanceTraveled
          │
          ▼
AccumulatedDistance hedeften küçük mü? ──Hayır──▶ Bitti: EffectivePlayRate hesapla
          │ Evet
          ▼
Curve'den 1/30 s'lik adımın mesafesini oku
          │
          ▼
Adım mesafesi negatif mi?
     ├─ Evet  → Zaman ilerler ama birikim GERİ gider ─┐
     └─ Hayır → Zaman ve birikim birlikte ilerler ────┤
                                                       ▼
                        AccumulatedTime klip süresini aştı mı?
                             ├─ Evet  → Abort + Warning, poz donar
                             └─ Hayır → başa dön (AccumulatedDistance kontrolü)

Kritik zayıflık: AnimationDistanceThisStep negatifse kod bunu ayıklamaz. AccumulatedDistance + (negatif) < DistanceTraveled her zaman doğrudur, dolayısıyla zaman tam bir adım ilerlerken birikim geriye gider. Sonuç: playhead o bölgede duramaz, üzerinden atlar.

Menzil yetmezse (satır 135):

Warning: Failed to advance distance of (%.2f) after (%.2f) seconds
         on anim sequence (%ls). Aborting.

→ poz o karede donar.

Stop — DistanceMatchToTarget

İkili arama. Varsayımlarını dosyanın kendisi yazar (satır 44-46):

// Some assumptions:
// - keys have unique values, so for a given value, it maps to a single position
//   on the timeline of the animation.
// - key values are sorted in increasing order.

İşaret çevirmesi fonksiyonun içindedir (satır 223):

// By convention, distance curves store the distance to a target as a negative value.
GetAnimPositionFromDistance(AnimSequence, -DistanceToTarget, DistanceCurveName);

→ Node’a pozitif “hedefe kalan mesafe” verilir, fonksiyon negatife çevirip arar.

Varsayım bozulursa uyarı çıkmaz. İkili arama yanlış key çiftini kucaklar ve poz timeline’da rastgele bir noktaya zıplar. Sessiz arıza.

PlayRateClamp — emniyet supabı

Start fonksiyonunun az bilinen parametresi:

float EffectivePlayRate = (TimeAfterDistanceTraveled - CurrentTime) / DeltaTime;
if (PlayRateClamp.X >= 0.0f && PlayRateClamp.X < PlayRateClamp.Y)
{
    EffectivePlayRate = FMath::Clamp(EffectivePlayRate, PlayRateClamp.X, PlayRateClamp.Y);
}

Distance matching bir kareyi 4× hızlandırmak isterse clamp buna izin vermez.

⚠️ Takas: clamp devreye girdiği anda mesafe eşleşmesi bozulur — ayak kayması geri gelir. Clamp bir çözüm değil, hasar sınırlayıcıdır. Curve doğruysa hiç devreye girmez.

Kurulum — üç sessiz tuzak

Üçü de hata vermeden sistemin çalışmamasına yol açar.

# Tuzak Belirti Kontrol
1 Sequence Evaluator’ın ExplicitTime’ı Always Dynamic değil Poz ilk karede donar, tüm sürücüler sağlıklı görünür Log: "value is not dynamic"
2 Curve compression codec yanlış Lookup sessizce çalışmaz, poz donar CurveCompressionSettings = UniformIndexable
3 Curve monotonik değil Atlama / zıplama Curve’ü key key oku

Always Dynamic

ExplicitTime varsayılan olarak constant-fold edilir. Fold’lanmış bir değer SetExplicitTime’ı reddeder; fonksiyon yeni zamanı hesaplar ama yazamaz. Her iki fonksiyon da aynı uyarıyı basar:

Warning: Could not set explicit time on sequence evaluator,
         value is not dynamic. Set it as Always Dynamic.

UniformIndexable codec

Distance curve’leri FAnimCurveBufferAccess üzerinden örneklenir; bu erişimi başka hiçbir codec desteklemez. Yanlış codec’te BufferCurveAccess.IsValid() false döner ve fonksiyon sessizce 0.f verir.

Bu projede bunun için UniformIndexable codec’li ayrı bir Curve Compression Settings asset’i (ACCS_UniformIndexable) kullanılır.

⚠️ 1 ve 2 birbirini gizler. Yalnızca codec’i düzeltirsen poz yine donar ve hiçbir şey değişmemiş gibi görünür. Bu ikisi bir kez tam bir oturum yedi — önce log’a bak, sonra grafiğe.

Curve nasıl üretilir, neyi garanti etmez

Window → Animation Data Modifiers → Distance Curve Modifier

Ön şart (DistanceCurveModifier.cpp:22-26):

"DistanceCurveModifier failed. Reason: Root motion is disabled on the animation (%ls)"

bEnableRootMotion kapalıysa modifier hiç çalışmaz. In-place klipler için curve’ü başka bir yoldan bake etmek gerekir.

Ne yapıyor: en düşük root hızının olduğu anı bulur, her kareyi o ana göre örnekler ve büyüklüğünü (CalculateMagnitude) alır; işareti o ana göre atar.

Ne yapmıyor: monotonluğu garanti etmiyor. Dosyanın kendi TODO’su sınırı itiraf ediyor (satır 8-11):

// TODO: This logic works decently for simple clips but it should be reworked to be
// more robust:
//  * It could detect pivot points by change in direction.
//  * It should also account for clips that have multiple stop/pivot points.

Sonuç: kaynak animasyon bozuksa yeniden bake etmek düzeltmez. Curve, root track’in sadık bir kaydıdır — hatayı curve’de değil, animasyonda ara.

İyi bir Start/Stop klibinin kuralları

  1. Root monotonik ilerlemeli. Koşuya başlamadan önceki ağırlık kaydırması root’a değil pelvis’e yazılır. Root yalnızca ilerlemeyi taşır.
  2. Süre ile mesafe orantılı dağılmalı. Sürenin yarısı mesafenin %3’ünü kapsıyorsa o yarı ekranda hiç görünmez.
  3. Bitiş hızı loop hızına eşit olmalı. Start klibi MaxWalkSpeed’de bitmeli.
  4. Menzil yeterli olmalı. Klibin kat ettiği yol, karakterin tam hıza ulaşana kadar aldığı yoldan kısa olmamalı — yoksa Aborting uyarısı ve donma.
  5. Loop’un da root motion’ı olmalı — Stride Warping kullanılacaksa şart, çünkü StrideScale türetmesi DesiredSpeed / AnimatedSpeed üzerinden yapılır.

Bozuk bir curve neye benzer — AS_Run_Start

Ölçülmüş veri, 16 key, 0.5 s.

İlk 0.3 saniye — çukur

t=0.000  |                                        0.00
t=0.033  |#                                       0.55
t=0.067  |###                                     1.90
t=0.100  |######                                  3.60
t=0.133  |#########                               5.20
t=0.167  |###########                             6.28
t=0.200  |############        <-- TEPE            6.48
t=0.233  |#########           <-- düşüyor         5.27
t=0.267  |#######             <-- ÇUKUR DİBİ      3.86
t=0.300  |###################                    10.74

0.200 → 0.267 arasında değer düşüyor. Start bölümünde anlatılan AdvanceTimeByDistanceMatching döngüsü bu bölgede duramaz.

Fiziksel sebebi (root bone Y ekseninde kare kare ölçüldü): karakter push-off’tan önce gerçekten geriye 6.5 cm gidiyor. İlk 0.19 s boyunca root hızı negatif (−8.8, −44.6, −49.7, −24.2 cm/s). Baker işaretsiz büyüklük yazdığı için curve başlangıç noktasından uzaklaşırken yükseliyor, geri dönerken düşüyor.

Tüm klip — dengesiz dağılım

t=0.000  |                                          0.00
t=0.100  |#                                         3.60
t=0.200  |##                                        6.48
t=0.267  |#                                         3.86   <-- sürenin %53'ü bitti
t=0.300  |###                                      10.74
t=0.333  |#######                                  23.17
t=0.367  |#############                            39.34
t=0.400  |###################                      59.19
t=0.433  |##########################               82.11
t=0.467  |###################################     109.09
t=0.500  |########################################125.06

Sürenin %53’ü mesafenin %3’ünü kapsıyor.

Bunun gameplay karşılığı

MaxAcceleration = 2048 ile karakter 3.86 cm’i t = sqrt(2d/a) = 0.061 s’de alır. Animasyonda bu 0.267 s’lik bölüm.

ANİMASYON     |------------------------|------------------------|
              0                     0.267 s                  0.5 s
              |    sürenin %53'ü       |    sürenin %47'si      |
              |      3.86 cm           |      121.2 cm          |

GAMEPLAY      |--|-------------------------------------------|
              0  0.061 s                                  0.355 s
                 ^
                 4.4x hızlandırma  -->  ekranda snap

Toplamda klibin 125 cm’i gameplay’de 0.355 s’de tükenir (klip 0.5 s), yani genel oran 1.4× — kabul edilebilir. Sorun oranın kendisi değil, dağılımı.

Teşhis rehberi

Ekranda görülen Muhtemel sebep İlk bakılacak yer
Poz ilk karede donuk ExplicitTime Always Dynamic değil Log: "not dynamic"
Poz donuk, log temiz Codec UniformIndexable değil Asset CurveCompressionSettings
Klip ortasında ani atlama Start curve’ü monotonik değil Curve key’leri
Timeline’da rastgele zıplama Stop curve’ünde tekrar eden değer Curve key’leri
Klip sonunda donma Menzil yetmiyor Log: "Failed to advance distance"
Poz akıcı ama ayak kayıyor PlayRateClamp devrede Node parametresi + curve
Anticipation hiç görünmüyor Süre/mesafe dağılımı bozuk Curve’ün ilk yarısı

Kaynaklar

Motor tarafı, Engine/Plugins/Animation/AnimationLocomotionLibrary/ eklentisinin kaynak ağacında:

Dosya Ne için
Source/Runtime/Private/AnimDistanceMatchingLibrary.cpp Her iki algoritmanın gövdesi, varsayımlar, uyarı metinleri
Source/Editor/Private/DistanceCurveModifier.cpp Curve üretimi, root motion ön şartı, TODO
Source/Editor/Public/DistanceCurveModifier.h İşaret konvansiyonunun tanımı

Proje tarafı: MaxWalkSpeed ve MaxAcceleration projenin CharacterMovementComponent alt sınıfından, DefaultMaxWalkSpeed / SprintMaxWalkSpeed locomotion bileşeninden, curve compression ayarı da ACCS_UniformIndexable asset’inden okunmuştur.

Rev Tarih Çizen Değişiklik
R-001 Alparslan İlk kayıt