İnterpolasyon aslında bir nesnenin hareketini düzeltmek için nasıl çalışır?


10

Son 8 ay içinde gerçek bir sevinç olmadan birkaç benzer soru sordum, bu yüzden soruyu daha genel hale getireceğim.

OpenGL ES 2.0 olan bir Android oyunum var. İçinde aşağıdaki Oyun Döngüsü var:

Döngüm sabit bir zaman adımı prensibi ile çalışır (dt = 1 / ticksPerSecond )

loops=0;

    while(System.currentTimeMillis() > nextGameTick && loops < maxFrameskip){

        updateLogic(dt);
        nextGameTick+=skipTicks;
        timeCorrection += (1000d/ticksPerSecond) % 1;
        nextGameTick+=timeCorrection;
        timeCorrection %=1;
        loops++;

    }

    render();   

Entegrasyonum şöyle çalışıyor:

sprite.posX+=sprite.xVel*dt;
sprite.posXDrawAt=sprite.posX*width;

Şimdi, her şey istediğim gibi çalışıyor. Bir nesnenin 2,5 saniye içinde belirli bir mesafe (ekran genişliği diyelim) boyunca hareket etmesini istediğimi belirtebilirim ve sadece bunu yapar. Ayrıca, oyun döngümde izin verdiğim kare atlama nedeniyle, bunu hemen hemen her cihazda yapabilirim ve her zaman 2.5 saniye sürecektir.

Sorun

Ancak sorun, bir render çerçevesi atlandığında grafik kekemeliğidir. Son derece sinir bozucu. Çerçeveleri atlama yeteneğini kaldırırsam, her şey istediğiniz gibi pürüzsüzdür, ancak farklı cihazlarda farklı hızlarda çalışır. Yani bu bir seçenek değil.

Çerçevenin neden atladığından hala emin değilim, ancak bunun kötü performansla ilgisi olmadığını belirtmek isterim, kodu hemen 1 küçük sprite ve mantık yok (mantık dışında gerekli) hareketli grafiği taşıyın) ve yine de kareleri atladım. Ve bu bir Google Nexus 10 tablette (ve yukarıda belirtildiği gibi, hızı cihazlarda tutarlı tutmak için çerçeve atlamaya ihtiyacım var).

Yani, sahip olduğum diğer tek seçenek enterpolasyon (veya ekstrapolasyon) kullanmaktır, orada her makaleyi okudum ama hiçbiri nasıl çalıştığını anlamama yardımcı olmadı ve denediğim tüm uygulamalar başarısız oldu.

Tek bir yöntem kullanarak her şeyin yolunda gitmesini sağlayabildim ama işime yaramadı çünkü çarpışmamı bozdu. Aynı sorunu herhangi bir benzer yöntemle öngörebilirim çünkü enterpolasyon, oluşturma yöntemine aktarılır (ve içinde hareket ettirilir) - oluşturma zamanında. Çarpışma düzeltir konumu (karakter şimdi hemen yanındaki duvara ayakta) Böylece, sonra oluşturucu 's konumunu değiştirmek ve bunu çizebilirsiniz içinde duvara.

Bu yüzden gerçekten kafam karıştı. İnsanlar, bir nesnenin konumunu hiçbir zaman oluşturma yönteminden değiştirmemeniz gerektiğini söylemişlerdir , ancak çevrimiçi tüm örnekler bunu göstermektedir.

Bu yüzden doğru yönde bir itme istiyorum, lütfen bu oyunu defalarca okuduğumdan popüler oyun döngüsü makalelerine (deWitters, Timestep'inizi düzeltin, vb.) Bağlamayın . Ben değilim değil benim için kod yazmak istememden. İnterpolasyonun bazı örneklerle gerçekte nasıl çalıştığını basit bir şekilde açıklayınız. Daha sonra gidip herhangi bir fikri koduma entegre etmeye çalışacağım ve gerekirse daha ayrıntılı sorular soracağım. (Eminim bu birçok insanın uğraştığı bir sorundur).

Düzenle

Bazı ek bilgiler - oyun döngüsünde kullanılan değişkenler.

private long nextGameTick = System.currentTimeMillis();
//loop counter
private int loops;
//Amount of frames that we will allow app to skip before logic is affected
private final int maxFrameskip = 5;                         
//Game updates per second
final int ticksPerSecond = 60;
//Amount of time each update should take        
private final int skipTicks = (1000 / ticksPerSecond);
float dt = 1f/ticksPerSecond;
private double timeCorrection;

Ve düşüşün nedeni ...................?
BungleBonce

1
Bazen söylemek imkansız. Bu, bir sorunu çözmeye çalışırken iyi bir sorunun olması gereken her şeye sahip gibi görünüyor. Kısa kod snippet'i, denediklerinizin açıklamaları, araştırma girişimleri ve probleminizin ne olduğunu ve neleri bilmeniz gerektiğini net bir şekilde açıklamak.
Jesse Dorsey

Senin vahşetin ben değildim, ama lütfen bir kısmını açıkla. Bir çerçeve atlandığında grafiklerin kesik olduğunu söylersiniz. Bu açık bir ifade gibi görünüyor (bir çerçeve kaçırılmış, bir çerçeve kaçırılmış gibi görünüyor). Yani atlamayı daha iyi açıklayabilir misin? Tuhaf bir şey olur mu? Değilse, bu çözülemez bir sorun olabilir, çünkü kare hızı düşerse yumuşak hareket elde edemezsiniz.
Seth Battin

Teşekkürler, Noctrine, insanlar bir açıklama bırakmadan aşağı inince gerçekten beni rahatsız ediyor. @SethBattin, üzgünüm, elbette, doğru, çerçeve atlama görüntünün kayması neden olduğunu edilir evet, ancak, bir tür enterpolasyon gerektiğini yukarıda dediğim gibi, ben başarıya bazı vardı (ama sınırlı) ettik bu sıralamak. Yanılıyorsam, o zaman soru olurdu, çeşitli cihazlarda aynı hızda sorunsuz bir şekilde çalışmasını nasıl sağlayabilirim?
BungleBonce

4
Bu belgeleri dikkatlice tekrar okuyun. Gerçekte oluşturma yönteminde nesnenin konumunu değiştirmezler. Yöntemin görünen konumunu yalnızca son konumundan ve geçerli konumundan ne kadar zaman geçtiğine bağlı olarak değiştirirler.
AttackingHobo

Yanıtlar:


5

Hareketin düzgün görünmesi için iki şey çok önemlidir, birincisi, oluşturduğunuz şeyin çerçevenin kullanıcıya sunulduğu sırada beklenen durumla eşleşmesi gerektiğidir, ikincisi kullanıcıya çerçeveler sunmanız gerekir göreceli olarak sabit bir aralıkta. T + 10ms'de bir çerçeve, sonra T + 30ms'de bir çerçeve, sonra T + 40ms'de bir çerçeve sunmak, o zamanlar için gerçekte gösterilen şey simülasyona göre doğru olsa bile kullanıcıya titriyor gibi görünecektir.

Ana döngünüzde yalnızca düzenli aralıklarla render ettiğinizden emin olmak için herhangi bir geçit mekanizması bulunmuyor gibi görünüyor. Bazen renderler arasında 3 güncelleme yapabilir, bazen 4 yapabilirsiniz. Temel olarak, döngünüz mevcut zamanın önüne itmek için yeterli zamanı simüle ettikten sonra mümkün olduğunca sık işleyecektir. sonra bu durumu oluşturun. Ancak güncelleme veya oluşturma süresinde ve çerçeveler arasındaki aralıkta da değişiklik olabilir. Simülasyonunuz için sabit bir zaman adımınız var, ancak oluşturma işleminiz için değişken bir zaman adımınız var.

Muhtemelen ihtiyacınız olan şey, render işleminizden hemen önce bir beklemedir; İdeal olarak bu uyarlanabilir olmalıdır: güncellemek / işlemek için çok uzun sürdüyseniz ve aralığın başlangıcı zaten geçtiyse, derhal işlemelisiniz, ancak sürekli olarak render edip güncelleyip hala devam edene kadar aralık uzunluğunu artırmalısınız. aralık bitmeden bir sonraki oluşturma. Yedeklemek için bol zamanınız varsa, daha hızlı işlemek için aralığı yavaşça azaltabilir (yani kare hızını artırabilirsiniz).

Ancak, işte bakıcı, simülasyon durumunun "şimdi" olarak güncellendiğini tespit ettikten hemen sonra çerçeveyi oluşturmazsanız, geçici takma adı uygularsınız. Kullanıcıya sunulan çerçeve biraz yanlış zamanda sunulur ve kendi içinde bir kekik gibi hissedilir.

Okuduğunuz makalelerde gördüğünüz "kısmi zaman adımının" nedeni budur. İyi bir nedenden dolayı oradadır ve bunun nedeni, fizik zaman testinizi sabit oluşturma zaman testinizin bazı sabit integral katlarına sabitlemezseniz, çerçeveleri doğru zamanda sunamazsınız. Sonuçta onları çok erken veya çok geç sunuyorsunuz. Sabit bir oluşturma oranı elde etmenin ve hala fiziksel olarak doğru olan bir şeyi sunmanın tek yolu, oluşturma aralığı ortaya çıktığında, büyük olasılıkla sabit fizik zamanlarınızın ikisinin ortasında olacağınızı kabul etmektir. Ancak bu, oluşturma sırasında nesnelerin değiştirildiği anlamına gelmez, yalnızca oluşturma işleminin, nesnelerin nerede olduklarını geçici olarak belirlemeleri gerekir; böylece, onları daha önce bulundukları yer ile güncellemeden sonraki konum arasında bir yerde oluşturabilir. Bu önemlidir - render için dünya durumunu asla değiştirmeyin, sadece güncellemeler dünya durumunu değiştirmelidir.

Yani bir sözde kod döngüsüne koymak için, daha fazla bir şeye ihtiyacınız olduğunu düşünüyorum:

InitialiseWorldState();

previousTime = currentTime = 0.0;
renderInterval = 1.0 / 60.0; //A nice high starting interval

subFrameProportion = 1.0; //100% currentFrame, 0% previousFrame

while (true)
{
    frameStart = ActualTime();

    //Render the world state as if it was some proportion 
    // between previousTime and currentTime
    // E.g. if subFrameProportion is 0.5, previousTime is 0.1 and 
    // currentTime is 0.2, then we actually want to render the state
    // as it would be at time 0.15. We'd do that by interpolating 
    // between movingObject.previousPosition and movingObject.currentPosition
    // with a lerp parameter of 0.5
    Render(subFrameProportion); 

    //Check we've not taken too long and missed our render interval
    frameTime = ActualTime() - frameStart;
    if (frameTime > renderInterval)
    {
        renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
    }

    expectedFrameEnd = frameStart + renderInterval;

    //Loop until it's time to render the next frame
    while (ActualTime() < expectedFrameEnd)
    {
        //step the simulation forward until it has moved just beyond the frame end
        if (previousTime < expectedFrameEnd) &&
            currentTime >= expectedFrameEnd)
        {
            previousTime = currentTime;

            Update();
            currentTime += fixedTimeStep;

            //After the update, all objects will be in the position they should be for
            // currentTime, **but** they also need to remember where they were before,
            // so that the rendering can draw them somewhere between previousTime and
            //  currentTime

            //Check again we've not taken too long and missed our render interval
            frameTime = ActualTime() - frameStart;
            if (frameTime > renderInterval)
            {
                renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
                expectedFrameEnd = frameStart + renderInterval
            }
        }
        else
        {
            //We've brought the simulation to just after the next time
            // we expect to render, so we just want to wait.
            // Ideally sleep or spin in a tight loop while waiting.
            timeTillFrameEnd = expectedFrameEnd - ActualTime();
            sleep(timeTillFrameEnd);
        }
    }

    //How far between update timesteps (i.e. previousTime and currentTime)
    // will we be at the end of the frame when we start the next render?
    subFrameProportion = (expectedFrameEnd - previousTime) / (currentTime - previousTime);
}

Bunun çalışabilmesi için güncellenen tüm nesnelerin nerede olduklarını ve şimdi nerede olduklarını bilmeleri gerekir, böylece render nesnenin nerede olduğu bilgisini kullanabilir.

class MovingObject
{
    Vector velocity;
    Vector previousPosition;
    Vector currentPosition;

    Initialise(startPosition, startVelocity)
    {
        currentPosition = startPosition; // position at time 0
        velocity = startVelocity;
        //ignore previousPosition because we should never render before time 0
    }

    Update()
    {
        previousPosition = currentPosition;
        currentPosition += velocity * fixedTimeStep;
    }

    Render(subFrameProportion)
    {
        Vector actualPosition = 
            Lerp(previousPosition, currentPosition, subFrameProportion);
        RenderAt(actualPosition);
    }
}

Ve oluşturma işleminin tamamlanması 3 ms sürüyor, güncelleme 1 ms sürüyor, güncelleme zaman adımınız 5 ms olarak sabitleniyor ve oluşturma zaman aşamanız 16 ms [60Hz] 'de başlıyor (ve kalıyor) diyerek milisaniye cinsinden bir zaman çizelgesi oluşturalım.

0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33
R0          U5  U10 U15 U20 W16                                 R16         U25 U30 U35 W32                                 R32
  1. İlk önce 0 zamanında başlatırız (yani currentTime = 0)
  2. Dünyayı 0 zamanında çekecek olan 1.0 (% 100 currentTime) oranı ile render yapıyoruz
  3. Bu bittiğinde, gerçek zaman 3'tür ve çerçevenin 16'ya kadar bitmesini beklemiyoruz, bu yüzden bazı güncellemeler yapmamız gerekiyor
  4. T + 3: 0'dan 5'e güncelleme yapıyoruz (bu nedenle daha sonra currentTime = 5, previousTime = 0)
  5. T + 4: çerçeve sonundan önce, bu yüzden 5'ten 10'a güncelliyoruz
  6. T + 5: çerçeve sonundan önce, bu yüzden 10'dan 15'e güncelleme yapıyoruz
  7. T + 6: çerçeve sonundan önce, bu yüzden 15'ten 20'ye güncelleme yapıyoruz
  8. T + 7: hala çerçeve sonundan önce, ancak currentTime kare sonunun hemen ötesinde. Daha fazla simüle etmek istemiyoruz çünkü bunu yapmak, bizi bir sonraki render etmek istediğimiz zamanın ötesine itecektir. Bunun yerine bir sonraki oluşturma aralığı için sessizce bekleriz (16)
  9. T + 16: Tekrar oluşturma zamanı. previousTime 15, currentTime 20'dir. Dolayısıyla, T + 16'da işlemek istiyorsak, 5 ms uzunluğunda 1 ms'lik yoldayız. Yani çerçevenin% 20'si kadar yoldayız (oran = 0.2). Oluşturduğumuzda, nesneleri önceki konumları ile mevcut konumları arasında% 20 oranında çizeriz.
  10. 3'e geri dönün ve süresiz olarak devam edin.

Burada çok önceden simüle etme konusunda başka bir nüans daha var, yani çerçeve gerçekten oluşturulmadan önce olmasına rağmen kullanıcının girdileri yok sayılabilir, ancak döngünün sorunsuz bir şekilde taklit edildiğinden emin olana kadar endişelenmeyin.


Not: sahte kod iki şekilde zayıftır. Birincisi, ölüm sarmal kasasını yakalamaz (fixTimeStep to Update'den daha uzun sürer, yani simülasyon artık geride kalır, etkili bir şekilde sonsuz bir döngü), ikincisi renderInterval bir daha asla kısalmaz. Pratikte renderInterval değerini hemen artırmak istiyorsunuz, ancak zamanla gerçek çerçeve süresine tolerans dahilinde olabildiğince kademeli olarak kısaltın. Aksi takdirde, bir kötü / uzun güncelleme sonsuza kadar düşük bir kare hızı ile sizi eyer.
MrCranky

Bu @MrCranky için teşekkürler, gerçekten, benim döngüde render 'sınırlamak' için yıllardır mücadele ediyorum! Sadece nasıl yapılacağını anlayamadım ve sorunlardan biri olup olmadığını merak ettim. Bunu doğru bir şekilde okuyacağım ve önerilerinizi deneyeceğim, rapor vereceğim! Tekrar teşekkürler :-)
BungleBonce

Teşekkürler @MrCranky, Tamam, cevabınızı okudum ve tekrar okudum ama anlayamıyorum :-( Uygulamayı denedim ama sadece boş bir ekran verdi. Gerçekten bununla mücadele ediyorum. PreviousFrame ve currentFrame varsayalım Ayrıca, hareketli nesnelerimin önceki ve geçerli konumlarıyla ilgilidir? Ayrıca, "currentFrame = Update ();" satırı hakkında ne - Bu satırı alamıyorum, bu arama update () anlamına mı geliyor? başka bir güncelleme mi arıyorum? Yoksa sadece currentFrame (pozisyon) yeni değerine ayarlamak anlamına mı geliyor? Yardımınız için tekrar teşekkürler !!
BungleBonce

Evet, etkili. PreviousFrame'i ve currentFrame'i Update ve InitialiseWorldState'ten dönüş değerleri olarak koymamın nedeni, renderlamanın dünyayı iki sabit güncelleme adımı arasında kısmen olduğu gibi çizmesine izin vermek için, çizmek istediğiniz nesnenin yanı sıra önceki konumlarını da gösterir. Her nesnenin her iki değeri de dahili olarak kaydetmesini sağlayabilirsiniz, bu da kullanılmaz hale gelir.
MrCranky

Ancak, T zamanındaki dünyanın mevcut durumunu temsil etmek için gereken tüm devlet bilgilerinin tek bir nesne altında tutulması için şeyleri mimar etmek de (ancak çok daha zor) mümkündür. Çerçeve durumunu bir güncelleme adımı tarafından üretilen bir şey olarak değerlendirebildiğiniz ve önceki çerçeveyi etrafta tutmak, bu çerçeve durumu nesnelerinden birini daha fazla tutmakla ilgili olduğundan, kavramsal olarak sistemde hangi bilgilerin bulunduğunu açıklarken çok daha temizdir. Ancak cevabı, muhtemelen uyguladığınız gibi biraz daha yeniden yazabilirim.
MrCranky

3

Herkesin size söylediği doğrudur. Hareketli grafiğinizdeki hareketli grafiğin simülasyon konumunu asla güncellemeyin.

Şöyle düşünün, hareketli grafiğinizin 2 konumu vardır; simülasyonun son simülasyon güncellemesinden itibaren olduğunu ve hareketli grafiğin oluşturulduğu yeri belirtir. Bunlar tamamen farklı iki koordinattır.

Sprite, tahmin edilen pozisyonunda oluşturulur. Ekstrapole edilmiş konum, hareketli grafiği oluşturmak için kullanılan her bir oluşturma karesi hesaplanır ve sonra atılır. Hepsi bu kadar.

Bunun dışında iyi bir anlayışa sahip görünüyorsunuz. Bu yardımcı olur umarım.


Excellent @WilliamMorrison - bunu onayladığınız için teşekkürler, durumun böyle olduğundan asla% 100 emin olamadım, şimdi bu çalışmayı bir dereceye kadar elde etme yolunda olduğumu düşünüyorum - şerefe!
BungleBonce

Sadece merakla @WilliamMorrison, bu fırlatma koordinatlarını kullanarak, spriteların diğer nesnelere 'gömülü' ya da 'hemen üstündeki' çizilme sorununu nasıl hafifletir - açık örnek, 2d oyununda katı nesneler. Çarpışma kodunuzu oluşturma zamanında da çalıştırmanız gerekir mi?
BungleBonce

Oyunlarımda evet, ben de öyle yapıyorum. Lütfen benden daha iyi ol, bunu yapma, en iyi çözüm değil. Kullanmaması gereken mantıkla oluşturma kodunu karmaşıklaştırır ve gereksiz çarpışma algılamasında CPU'yu boşa harcar. İkinci konumdan son konuma ve geçerli konum arasında enterpolasyon yapmak daha iyi olur. Bu, sorunu kötü bir pozisyonda tahmin edemediğiniz için çözer, ancak simülasyonun bir adım gerisinde olduğundan işleri karmaşık hale getirir. Fikrinizi, hangi yaklaşımı benimsediğinizi ve deneyimlerinizi duymayı seviyorum.
William Morrison

Evet, çözülmesi zor bir problem. Bununla ilgili ayrı bir soru sordum burada gamedev.stackexchange.com/questions/83230/… Bir göz atmak veya bir şey katkıda bulunmak istiyorsanız. Şimdi, yorumunuzda önerdiğiniz şey, bunu zaten yapmıyor muyum? (Önceki ve geçerli kare arasında enterpolasyon)?
BungleBonce

Pek değil. Aslında şu anda tahmin edersiniz. Simülasyondan en güncel verileri alırsınız ve kesirli zaman adımlarından sonra verilerin nasıl göründüğünü tahmin edersiniz. Son simülasyon konumu ile mevcut simülasyon konumu arasında renderleme için kesirli zaman aralıklarıyla enterpolasyon yapmanızı öneririm. Oluşturma 1 zaman aşımı ile simülasyonun arkasında olacak. Bu, simülasyonun doğrulamadığı bir durumda hiçbir nesneyi asla oluşturmayacağınızı garanti eder (yani, simülasyon başarısız olmadıkça bir mermi duvarda görünmez.)
William Morrison
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.