Làm thế nào để nội suy thực sự hoạt động để làm mịn chuyển động của một đối tượng?


10

Tôi đã hỏi một vài câu hỏi tương tự trong 8 tháng qua hoặc không có niềm vui thực sự, vì vậy tôi sẽ làm cho câu hỏi trở nên chung chung hơn.

Tôi có một trò chơi Android là OpenGL ES 2.0. trong đó tôi có Vòng lặp trò chơi sau:

Vòng lặp của tôi hoạt động theo nguyên tắc bước thời gian cố định (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();   

Sự kết hợp của tôi hoạt động như thế này:

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

Bây giờ, mọi thứ hoạt động khá nhiều như tôi muốn. Tôi có thể chỉ định rằng tôi muốn một đối tượng di chuyển qua một khoảng cách nhất định (chiều rộng màn hình nói) trong 2,5 giây và nó sẽ làm điều đó. Ngoài ra do bỏ qua khung hình mà tôi cho phép trong vòng lặp trò chơi của mình, tôi có thể thực hiện việc này trên hầu hết mọi thiết bị và sẽ luôn mất 2,5 giây.

Vấn đề

Tuy nhiên, vấn đề là khi một khung hình render bỏ qua, đồ họa bị vấp. Nó cực kỳ khó chịu. Nếu tôi loại bỏ khả năng bỏ qua các khung hình, thì mọi thứ sẽ mượt mà như bạn muốn, nhưng sẽ chạy ở các tốc độ khác nhau trên các thiết bị khác nhau. Vì vậy, nó không phải là một lựa chọn.

Tôi vẫn không chắc tại sao khung hình bị bỏ qua, nhưng tôi muốn chỉ ra rằng điều này không liên quan gì đến hiệu suất kém , tôi đã lấy mã trở lại 1 sprite nhỏ và không có logic (ngoài logic cần thiết để di chuyển sprite) và tôi vẫn bị bỏ qua các khung. Và đây là trên máy tính bảng Google Nexus 10 (và như đã đề cập ở trên, tôi cần bỏ qua khung hình để giữ tốc độ ổn định trên mọi thiết bị).

Vì vậy, lựa chọn duy nhất khác mà tôi có là sử dụng phép nội suy (hoặc ngoại suy), tôi đã đọc mọi bài báo ở đó nhưng không có gì thực sự giúp tôi hiểu cách thức hoạt động và tất cả các triển khai đã cố gắng của tôi đều thất bại.

Sử dụng một phương pháp tôi có thể khiến mọi thứ chuyển động trơn tru nhưng không thể thực hiện được vì nó làm hỏng sự va chạm của tôi. Tôi có thể thấy trước vấn đề tương tự với bất kỳ phương thức tương tự nào vì phép nội suy được truyền cho (và được thực hiện bên trong) phương thức kết xuất - tại thời điểm kết xuất. Vì vậy, nếu Collision điều chỉnh vị trí (nhân vật hiện đang đứng ngay cạnh tường), thì trình kết xuất có thể thay đổi vị trí của nó và vẽ nó vào tường.

Vì vậy, tôi thực sự bối rối. Mọi người đã nói rằng bạn không bao giờ nên thay đổi vị trí của một đối tượng từ bên trong phương thức kết xuất, nhưng tất cả các ví dụ trực tuyến đều cho thấy điều này.

Vì vậy, tôi đang yêu cầu đẩy đúng hướng, vui lòng không liên kết đến các bài viết vòng lặp trò chơi phổ biến (deWitters, Sửa dấu thời gian của bạn, v.v.) vì tôi đã đọc nhiều lần . Tôi không yêu cầu ai viết mã cho tôi. Chỉ cần giải thích một cách đơn giản về cách thức Nội suy thực sự hoạt động với một số ví dụ. Sau đó tôi sẽ đi và cố gắng tích hợp bất kỳ ý tưởng nào vào mã của mình và sẽ hỏi những câu hỏi cụ thể hơn nếu cần - tiếp tục đi xuống. (Tôi chắc chắn đây là vấn đề mà nhiều người đấu tranh).

biên tập

Một số thông tin bổ sung - các biến được sử dụng trong vòng lặp trò chơi.

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;

Và lý do của downvote là ...................?
BungleBonce

1
Đôi khi không thể nói. Điều này có vẻ như có tất cả mọi thứ một câu hỏi hay khi cố gắng giải quyết vấn đề. Đoạn mã ngắn gọn, giải thích về những gì bạn đã cố gắng, nghiên cứu cố gắng và giải thích rõ ràng vấn đề của bạn là gì và những gì bạn cần biết.
Jesse Dorsey

Tôi không phải là downvote của bạn, nhưng xin vui lòng làm rõ một phần. Bạn nói rằng đồ họa bị vấp khi khung hình bị bỏ qua. Đó dường như là một tuyên bố rõ ràng (một khung hình bị bỏ lỡ, có vẻ như một khung hình bị bỏ qua). Vì vậy, bạn có thể giải thích tốt hơn về việc bỏ qua? Có điều gì đó kỳ lạ xảy ra? Nếu không, đây có thể là một vấn đề không thể giải quyết được, bởi vì bạn không thể có được chuyển động trơn tru nếu tốc độ khung hình giảm xuống.
Seth Battin

Cảm ơn, Noctrine, nó thực sự làm tôi khó chịu khi mọi người downvote mà không để lại lời giải thích. @SethBattin, xin lỗi, vâng tất nhiên, bạn đang đúng, bỏ qua khung đang gây ra sự xóc nảy lên, tuy nhiên, nội suy của một số loại nên sắp xếp này ra, như tôi nói ở trên, tôi đã có một số (nhưng hạn chế) thành công. Nếu tôi sai, thì tôi đoán câu hỏi sẽ là, làm thế nào tôi có thể khiến nó chạy trơn tru ở cùng tốc độ trên nhiều thiết bị khác nhau?
BungleBonce

4
Đọc lại những tài liệu đó một cách cẩn thận. Họ không thực sự sửa đổi vị trí của đối tượng trong phương thức kết xuất. Họ chỉ sửa đổi vị trí rõ ràng của phương pháp dựa trên vị trí cuối cùng và vị trí hiện tại của nó dựa trên thời gian đã trôi qua.
Tấn

Câu trả lời:


5

Có hai điều cốt yếu để chuyển động xuất hiện trơn tru, thứ nhất rõ ràng là những gì bạn kết xuất cần phù hợp với trạng thái mong đợi tại thời điểm khung được hiển thị cho người dùng, thứ hai là bạn cần trình bày khung cho người dùng tại một khoảng thời gian tương đối cố định. Trình bày một khung hình ở T + 10ms, sau đó một khung hình khác tại T + 30ms, sau đó một khung hình khác tại T + 40ms, sẽ xuất hiện cho người dùng đang bị lúng túng, ngay cả khi những gì thực sự hiển thị cho những thời điểm đó là chính xác theo mô phỏng.

Vòng lặp chính của bạn dường như thiếu bất kỳ cơ chế gating nào để đảm bảo rằng bạn chỉ kết xuất theo chu kỳ. Vì vậy, đôi khi bạn có thể thực hiện 3 lần cập nhật giữa các lần kết xuất, đôi khi bạn có thể thực hiện 4. Về cơ bản, vòng lặp của bạn sẽ hiển thị thường xuyên nhất có thể, ngay khi bạn mô phỏng đủ thời gian để đẩy trạng thái mô phỏng trước thời điểm hiện tại, bạn sẽ sau đó kết xuất trạng thái đó. Nhưng bất kỳ sự thay đổi nào trong thời gian cần để cập nhật hoặc kết xuất, và khoảng thời gian giữa các khung cũng sẽ khác nhau. Bạn đã có một dấu thời gian cố định cho mô phỏng của mình, nhưng dấu thời gian thay đổi cho kết xuất của bạn.

Những gì bạn có thể cần là chờ đợi ngay trước khi kết xuất, điều đó đảm bảo rằng bạn chỉ bắt đầu kết xuất khi bắt đầu một khoảng thời gian kết xuất. Lý tưởng nhất là phải thích ứng: nếu bạn mất quá nhiều thời gian để cập nhật / kết xuất và bắt đầu khoảng thời gian đã trôi qua, bạn nên kết xuất ngay lập tức, nhưng cũng tăng thời lượng khoảng thời gian, cho đến khi bạn có thể kết xuất và cập nhật một cách nhất quán kết xuất tiếp theo trước khi khoảng thời gian kết thúc. Nếu bạn có nhiều thời gian rảnh rỗi, thì bạn có thể từ từ giảm khoảng thời gian (tức là tăng tốc độ khung hình) để hiển thị lại nhanh hơn.

Nhưng, và đây là kicker, nếu bạn không kết xuất khung ngay lập tức sau khi phát hiện trạng thái mô phỏng đã được cập nhật thành "ngay bây giờ", thì bạn sẽ giới thiệu răng cưa tạm thời. Khung được trình bày cho người dùng đang được trình bày không đúng lúc, và bản thân nó sẽ cảm thấy như bị nói lắp.

Đây là lý do cho "dấu thời gian một phần" bạn sẽ thấy được đề cập trong các bài viết bạn đã đọc. Đó là một lý do chính đáng, và đó là vì trừ khi bạn sửa dấu thời gian vật lý của mình thành một số tích phân cố định của dấu thời gian kết xuất cố định, bạn chỉ đơn giản là không thể hiển thị các khung vào đúng thời điểm. Bạn kết thúc hoặc trình bày chúng quá sớm, hoặc quá muộn. Cách duy nhất để có được tốc độ kết xuất cố định và vẫn hiển thị thứ gì đó chính xác về mặt vật lý, là chấp nhận rằng tại thời điểm kết xuất xuất hiện, rất có thể bạn sẽ ở giữa hai dấu thời gian vật lý cố định của mình. Nhưng điều đó không có nghĩa là các đối tượng được sửa đổi trong khi kết xuất, chỉ là kết xuất phải tạm thời thiết lập vị trí của các đối tượng để nó có thể hiển thị chúng ở một nơi nào đó ở giữa nơi chúng ở trước và nơi chúng ở sau khi cập nhật. Điều đó quan trọng - không bao giờ thay đổi trạng thái thế giới để kết xuất, chỉ cập nhật mới thay đổi trạng thái thế giới.

Vì vậy, để đặt nó vào một vòng lặp mã giả, tôi nghĩ bạn cần một cái gì đó giống như:

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);
}

Để làm việc này, tất cả các đối tượng đang được cập nhật cần phải bảo tồn kiến ​​thức về nơi chúng ở trước và nơi chúng hiện tại, để kết xuất có thể sử dụng kiến ​​thức về vị trí của đối tượng.

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);
    }
}

Và hãy đặt ra dòng thời gian tính bằng mili giây, nói rằng quá trình kết xuất mất 3ms để hoàn thành, quá trình cập nhật mất 1ms, bước thời gian cập nhật của bạn được cố định thành 5ms và dấu thời gian kết xuất của bạn bắt đầu (và duy trì) ở 16ms [60Hz].

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. Đầu tiên chúng tôi khởi tạo tại thời điểm 0 (vì vậy currentTime = 0)
  2. Chúng tôi kết xuất với tỷ lệ 1.0 (100% currentTime), sẽ thu hút thế giới tại thời điểm 0
  3. Khi kết thúc, thời gian thực tế là 3 và chúng tôi không hy vọng khung hình sẽ kết thúc đến 16, vì vậy chúng tôi cần chạy một số cập nhật
  4. T + 3: Chúng tôi cập nhật từ 0 đến 5 (vì vậy sau đó currentTime = 5, trướcTime = 0)
  5. T + 4: vẫn còn trước khi kết thúc khung, vì vậy chúng tôi cập nhật từ 5 đến 10
  6. T + 5: vẫn còn trước khi kết thúc khung, vì vậy chúng tôi cập nhật từ 10 đến 15
  7. T + 6: vẫn còn trước khi kết thúc khung, vì vậy chúng tôi cập nhật từ 15 đến 20
  8. T + 7: vẫn ở trước đầu khung, nhưng currentTime chỉ vượt ra ngoài đầu khung. Chúng tôi không muốn mô phỏng thêm nữa bởi vì làm như vậy sẽ đẩy chúng tôi vượt quá thời gian tiếp theo chúng tôi muốn kết xuất. Thay vào đó, chúng tôi lặng lẽ chờ khoảng thời gian kết xuất tiếp theo (16)
  9. T + 16: Đã đến lúc kết xuất lại. trướcTime là 15, currentTime là 20. Vì vậy, nếu chúng ta muốn kết xuất ở T + 16, chúng ta sẽ đi được 1ms trong khoảng thời gian dài 5ms. Vì vậy, chúng tôi là 20% của cách thông qua khung (tỷ lệ = 0,2). Khi chúng ta kết xuất, chúng ta vẽ các đối tượng 20% ​​giữa vị trí trước đó và vị trí hiện tại của chúng.
  10. Lặp lại 3. và tiếp tục vô thời hạn.

Có một sắc thái khác ở đây về việc mô phỏng quá xa trước thời hạn, có nghĩa là đầu vào của người dùng có thể bị bỏ qua mặc dù chúng đã xảy ra trước khi khung hình thực sự được hiển thị, nhưng đừng lo lắng về điều đó cho đến khi bạn tự tin rằng vòng lặp đang mô phỏng trơn tru.


NB: mã giả yếu theo hai cách. Đầu tiên, nó không bắt được trường hợp xoắn ốc chết (mất nhiều thời gian hơn fixedTimeStep để cập nhật, nghĩa là mô phỏng rơi xa hơn nữa, thực sự là một vòng lặp vô hạn), thứ hai là renderInterval không bao giờ bị rút ngắn nữa. Trong thực tế, bạn muốn tăng renderInterval ngay lập tức, nhưng sau đó theo thời gian sẽ rút ngắn dần dần hết mức có thể trong phạm vi dung sai của thời gian khung hình thực tế. Nếu không, một bản cập nhật xấu / dài sẽ khiến bạn yên với tốc độ khung hình thấp mãi mãi.
MrCranky

Cảm ơn vì điều này @MrCranky, thực sự, tôi đã đấu tranh lâu dài về cách 'giới hạn' kết xuất trong vòng lặp của mình! Chỉ không thể tìm ra cách để làm điều đó và tự hỏi nếu đó có thể là một trong những vấn đề. Tôi sẽ có một bài đọc thích hợp thông qua điều này và thử đề xuất của bạn, sẽ báo cáo lại! Cảm ơn một lần nữa :-)
BungleBonce

Cảm ơn @MrCranky, OK, tôi đã đọc và đọc lại câu trả lời của bạn nhưng tôi không thể hiểu được :-( Tôi đã cố gắng thực hiện nó nhưng nó chỉ cho tôi một màn hình trống. Thực sự vật lộn với điều này. Liên quan đến vị trí trước đây và hiện tại của các đối tượng chuyển động của tôi? Ngoài ra, còn về dòng "currentFrame = Update ();" - Tôi không nhận được dòng này, điều này có nghĩa là cập nhật cuộc gọi (); vì tôi không thể thấy ở đâu khác tôi đang gọi cập nhật? Hay nó chỉ có nghĩa là đặt currentFrame (vị trí) thành giá trị mới? Cảm ơn một lần nữa vì sự giúp đỡ của bạn !!
BungleBonce

Vâng, hiệu quả. Lý do tại sao tôi đặt trướcFrame và currentFrame làm giá trị trả về từ Update và InitialiseWorldState là vì để cho phép kết xuất vẽ thế giới vì nó là một phần giữa hai bước cập nhật cố định, bạn không chỉ có vị trí hiện tại của mỗi bước đối tượng bạn muốn vẽ, nhưng cũng có vị trí trước đó của họ. Bạn có thể có mỗi đối tượng lưu cả hai giá trị bên trong, điều này trở nên khó sử dụng.
MrCranky

Nhưng cũng có thể (nhưng khó hơn nhiều) để kiến ​​trúc sư mọi thứ sao cho tất cả thông tin trạng thái cần thiết để thể hiện trạng thái hiện tại của thế giới tại thời điểm T được lưu giữ dưới một đối tượng. Về mặt khái niệm sẽ sạch hơn rất nhiều khi giải thích những thông tin xung quanh hệ thống vì bạn có thể coi trạng thái khung là một thứ được tạo ra bởi một bước cập nhật và giữ cho khung hình trước đó chỉ là giữ lại một trong những đối tượng trạng thái khung đó. Tuy nhiên tôi có thể viết lại câu trả lời giống như bạn thực sự có thể thực hiện nó.
MrCranky

3

Những gì mọi người đã nói với bạn là chính xác. Không bao giờ cập nhật vị trí mô phỏng của sprite trong logic kết xuất của bạn.

Hãy nghĩ về nó như thế này, sprite của bạn có 2 vị trí; trong đó mô phỏng cho biết anh ta là bản cập nhật mô phỏng cuối cùng và là nơi sprite được hiển thị. Chúng là hai tọa độ hoàn toàn khác nhau.

Sprite được hiển thị ở vị trí ngoại suy của mình. Vị trí ngoại suy được tính toán từng khung kết xuất, được sử dụng để kết xuất sprite, sau đó ném đi. Thats tất cả để có nó.

Ngoài ra, bạn dường như có một sự hiểu biết tốt. Hi vọng điêu nay co ich.


Tuyệt vời @WilliamMorrison - cảm ơn vì đã xác nhận điều này, tôi không bao giờ thực sự chắc chắn 100% rằng đây là trường hợp, bây giờ tôi nghĩ rằng tôi đang trên đường để làm việc này ở một mức độ nào đó - chúc mừng!
BungleBonce

Chỉ tò mò @WilliamMorrison, bằng cách sử dụng các tọa độ vứt bỏ này, làm thế nào để giảm thiểu vấn đề của các họa tiết được vẽ 'nhúng vào' hoặc 'ngay phía trên' các đối tượng khác - ví dụ rõ ràng, là vật thể rắn trong trò chơi 2d. Bạn có phải chạy mã va chạm của mình vào thời gian kết xuất không?
BungleBonce

Trong các trò chơi của tôi, vâng, đó là những gì tôi làm. Xin hãy tốt hơn tôi, đừng làm vậy, đó không phải là giải pháp tốt nhất. Nó làm phức tạp mã kết xuất với logic mà nó không nên sử dụng và sẽ lãng phí cpu khi phát hiện va chạm dư thừa. Tốt hơn là nên nội suy giữa vị trí thứ hai đến vị trí cuối cùng và vị trí hiện tại. Điều này giải quyết vấn đề vì bạn không ngoại suy đến một vị trí xấu, nhưng lại làm phức tạp mọi thứ khi bạn đang đưa ra một bước phía sau mô phỏng. Id thích nghe ý kiến ​​của bạn, cách tiếp cận bạn thực hiện và kinh nghiệm của bạn.
William Morrison

Vâng, đó là một vấn đề khó giải quyết. Tôi đã hỏi một câu hỏi riêng về vấn đề này ở đây gamedev.stackexchange.com/questions/83230/iêu nếu bạn muốn để mắt đến nó hoặc đóng góp một cái gì đó. Bây giờ, những gì bạn đề nghị trong bình luận của bạn, tôi đã không làm điều này chưa? (Nội suy giữa khung trước và khung hiện tại)?
BungleBonce

Không hẳn. Bạn đang thực sự ngoại suy ngay bây giờ. Bạn lấy dữ liệu mới nhất từ ​​mô phỏng và ngoại suy dữ liệu đó trông như thế nào sau dấu thời gian phân đoạn. Tôi đề nghị bạn nội suy giữa vị trí mô phỏng cuối cùng và vị trí mô phỏng hiện tại bằng dấu thời gian phân đoạn để hiển thị thay thế. Kết xuất sẽ ở phía sau mô phỏng theo 1 dấu thời gian. Điều này đảm bảo bạn sẽ không bao giờ khiến đối tượng ở trạng thái mô phỏng không xác thực (ví dụ: Một viên đạn sẽ không xuất hiện trên tường trừ khi mô phỏng không thành công.)
William Morrison
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.