Distance Matching — Start & Stop
Locomotion R-001 Çizen: Alparslan TamamLocomotion'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ı
- 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.
- Süre ile mesafe orantılı dağılmalı. Sürenin yarısı mesafenin %3’ünü kapsıyorsa o yarı ekranda hiç görünmez.
- Bitiş hızı loop hızına eşit olmalı. Start klibi
MaxWalkSpeed’de bitmeli. - Menzil yeterli olmalı. Klibin kat ettiği yol, karakterin tam hıza ulaşana
kadar aldığı yoldan kısa olmamalı — yoksa
Abortinguyarısı ve donma. - Loop’un da root motion’ı olmalı — Stride Warping kullanılacaksa şart,
çünkü
StrideScaletüretmesiDesiredSpeed / 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 |