Tốc độ khung hình đang ảnh hưởng đến tốc độ của vật thể


9

Tôi đang thử nghiệm xây dựng một công cụ trò chơi từ đầu trong Java và tôi có một vài câu hỏi. Vòng lặp trò chơi chính của tôi trông như thế này:

        int FPS = 60;
        while(isRunning){
            /* Current time, before frame update */
            long time = System.currentTimeMillis();
            update();
            draw();
            /* How long each frame should last - time it took for one frame */
            long delay = (1000 / FPS) - (System.currentTimeMillis() - time);
            if(delay > 0){
                try{
                    Thread.sleep(delay);
                }catch(Exception e){};
            }
        }

Như bạn có thể thấy, tôi đã đặt tốc độ khung hình ở mức 60FPS, được sử dụng trong delaytính toán. Độ trễ đảm bảo rằng mỗi khung hình sẽ mất cùng một khoảng thời gian trước khi kết xuất khung hình tiếp theo. Trong update()hàm của tôi, tôi làm x++tăng giá trị ngang của một đối tượng đồ họa mà tôi vẽ bằng cách sau:

bbg.drawOval(x,40,20,20);

Điều làm tôi bối rối là tốc độ. Khi tôi đặt FPSthành 150, vòng tròn được hiển thị có tốc độ rất nhanh, trong khi cài đặt FPSthành 30 di chuyển trên màn hình với tốc độ bằng một nửa. Không khung hình chỉ ảnh hưởng đến "độ mịn" của kết xuất và không phải tốc độ của các đối tượng được kết xuất? Tôi nghĩ rằng tôi đang thiếu một phần lớn, tôi muốn làm rõ một số điều.


4
Đây là một bài viết hay về vòng lặp trò chơi: sửa dấu thời gian của bạn
Regent Regent

2
Là một lưu ý phụ, chúng tôi thường cố gắng đặt những thứ không được thực hiện mỗi vòng lặp bên ngoài các vòng lặp. Trong mã của bạn, phép 1000 / FPSchia của bạn có thể được thực hiện và kết quả được gán cho một biến trướcwhile(isRunning) vòng lặp của bạn . Điều này giúp tiết kiệm một vài hướng dẫn CPU để làm việc gì đó nhiều hơn một lần vô ích.
Vaillancourt

Câu trả lời:


21

Bạn đang di chuyển vòng tròn một pixel trên mỗi khung hình. Không có gì đáng ngạc nhiên khi, nếu vòng lặp kết xuất của bạn chạy ở tốc độ 30 FPS, vòng tròn của bạn sẽ di chuyển 30 ở pixel mỗi giây.

Về cơ bản, bạn có ba cách có thể để giải quyết vấn đề này:

  1. Chỉ cần chọn một tốc độ khung hình và bám vào nó. Đó là điều mà rất nhiều game cũ đã làm - chúng chạy ở tốc độ cố định 50 hoặc 60 FPS, thường được đồng bộ hóa với tốc độ làm mới màn hình và chỉ thiết kế logic trò chơi của chúng để làm mọi thứ cần thiết trong khoảng thời gian cố định đó. Nếu, vì một số lý do, điều đó đã không xảy ra, trò chơi sẽ phải bỏ qua một khung (hoặc có thể bị sập), làm chậm hiệu quả cả bản vẽ và vật lý trò chơi xuống một nửa tốc độ.

    Đặc biệt, các trò chơi sử dụng các tính năng như phát hiện va chạm sprite phần cứng khá nhiều phải hoạt động như thế này, bởi vì logic trò chơi của chúng gắn chặt với kết xuất, được thực hiện trong phần cứng ở tốc độ cố định.

  2. Sử dụng một dấu thời gian thay đổi cho vật lý trò chơi của bạn. Về cơ bản, điều này có nghĩa là viết lại vòng lặp trò chơi của bạn để trông giống như thế này:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        float timestep = 0.001 * (time - lastTime);  // in seconds
        if (timestep <= 0 || timestep > 1.0) {
            timestep = 0.001;  // avoid absurd time steps
        }
        update(timestep);
        draw();
        // ... sleep until next frame ...
        lastTime = time;
    }
    

    và, bên trong update(), điều chỉnh các công thức vật lý để giải thích cho dấu thời gian biến, ví dụ như thế này:

    speed += timestep * acceleration;
    position += timestep * (speed - 0.5 * timestep * acceleration);
    

    Một vấn đề với phương pháp này là có thể khó để giữ cho vật lý (phần lớn) không phụ thuộc vào dấu thời gian ; bạn thực sự không muốn khoảng cách người chơi có thể nhảy phụ thuộc vào tốc độ khung hình của họ. Công thức tôi đã trình bày ở trên hoạt động tốt cho gia tốc không đổi, ví dụ như dưới trọng lực (và công thức trong bài được liên kết hoạt động khá tốt ngay cả khi gia tốc thay đổi theo thời gian), nhưng ngay cả với các công thức vật lý hoàn hảo nhất có thể, làm việc với phao có khả năng tạo ra một chút "nhiễu số", đặc biệt, có thể làm cho các replay chính xác là không thể. Nếu đó là điều bạn nghĩ rằng bạn có thể muốn, bạn có thể muốn các phương pháp khác.

  3. Tách rời các bản cập nhật và vẽ các bước. Ở đây, ý tưởng là bạn cập nhật trạng thái trò chơi của mình bằng dấu thời gian cố định, nhưng chạy một số lượng cập nhật khác nhau giữa mỗi khung. Đó là, vòng lặp trò chơi của bạn có thể trông giống như thế này:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        if (time - lastTime > 1000) {
            lastTime = time;  // we're too far behind, catch up
        }
        int updatesNeeded = (time - lastTime) / updateInterval;
        for (int i = 0; i < updatesNeeded; i++) {
            update();
            lastTime += updateInterval;
        }
        draw();
        // ... sleep until next frame ...
    }
    

    Để làm cho chuyển động nhận thức mượt mà hơn, bạn cũng có thể muốn draw()phương thức của mình nội suy mọi thứ như vị trí đối tượng một cách trơn tru giữa các trạng thái trò chơi trước và tiếp theo. Điều này có nghĩa là bạn cần truyền bù nội suy chính xác cho draw()phương thức, ví dụ như thế này:

        int remainder = (time - lastTime) % updateInterval;
        draw( (float)remainder / updateInterval );  // scale to 0.0 - 1.0
    

    Bạn cũng cần phải có update()phương pháp của mình thực sự tính toán trạng thái trò chơi trước một bước (hoặc có thể là một vài, nếu bạn muốn thực hiện phép nội suy spline bậc cao) và để nó lưu các vị trí đối tượng trước đó trước khi cập nhật chúng, để draw()phương thức có thể nội suy giữa họ. (Cũng có thể ngoại suy các vị trí dự đoán dựa trên vận tốc và gia tốc của vật thể, nhưng điều này có thể trông giật, đặc biệt nếu các vật thể di chuyển theo những cách phức tạp, khiến cho các dự đoán thường thất bại.)

    Một lợi thế của nội suy là, đối với một số loại trò chơi, nó có thể cho phép bạn giảm đáng kể tốc độ cập nhật logic trò chơi, trong khi vẫn duy trì ảo giác về chuyển động mượt mà. Ví dụ: bạn chỉ có thể cập nhật trạng thái trò chơi của mình, giả sử, 5 lần mỗi giây, trong khi vẫn vẽ 30 ​​đến 60 khung hình nội suy mỗi giây. Khi bạn làm điều này, bạn cũng có thể muốn xem xét xen kẽ logic trò chơi của mình với bản vẽ (nghĩa là có một tham số cho update()phương thức của bạn để chỉ cho nó chạy x % bản cập nhật đầy đủ trước khi quay lại) và / hoặc chạy vật lý trò chơi / logic và mã kết xuất trong các luồng riêng biệt (hãy cẩn thận với sự cố đồng bộ hóa!).

Tất nhiên, cũng có thể kết hợp các phương pháp này theo nhiều cách khác nhau. Ví dụ: trong trò chơi nhiều người chơi máy chủ của khách hàng, bạn có thể có máy chủ (không cần vẽ bất cứ thứ gì) chạy các bản cập nhật của nó theo dấu thời gian cố định (đối với vật lý nhất quán và khả năng chơi lại chính xác), trong khi máy khách thực hiện cập nhật dự đoán (để bị ghi đè bởi máy chủ, trong trường hợp có bất kỳ sự bất đồng nào) tại dấu thời gian thay đổi để có hiệu suất tốt hơn. Cũng có thể kết hợp một cách hữu ích các phép cập nhật nội suy và biến thời gian; ví dụ, trong kịch bản máy chủ của máy khách vừa mô tả, thực sự không có nhiều điểm trong việc máy khách sử dụng dấu thời gian cập nhật ngắn hơn máy chủ, do đó bạn có thể đặt giới hạn thấp hơn cho dấu thời gian của máy khách và nội suy trong giai đoạn vẽ để cho phép cao hơn FPS.

(Chỉnh sửa: Đã thêm mã để tránh các khoảng / lần cập nhật vô lý, trong trường hợp, máy tính tạm thời bị treo hoặc bị đóng băng trong hơn một giây trong khi vòng lặp trò chơi đang chạy. Cảm ơn Mooing Duck đã nhắc nhở tôi về sự cần thiết của điều đó .)


1
Cảm ơn bạn rất nhiều vì đã dành thời gian để trả lời câu hỏi của tôi, tôi thực sự đánh giá cao nó. Tôi thực sự thích cách tiếp cận của # 3, nó có ý nghĩa nhất đối với tôi. Hai câu hỏi, updateInterval được định nghĩa bởi cái gì và tại sao bạn chia cho nó?
Rugfizz

1
@Carpetfizz: updateIntervalchỉ là số mili giây bạn muốn giữa các bản cập nhật trạng thái trò chơi. Đối với, giả sử, 10 cập nhật mỗi giây, bạn đã đặt updateInterval = (1000 / 10) = 100.
Ilmari Karonen

1
currentTimeMilliskhông phải là một chiếc đồng hồ đơn điệu. Sử dụng nanoTimethay thế, trừ khi bạn muốn đồng bộ hóa thời gian mạng để làm rối loạn tốc độ của mọi thứ trong trò chơi của bạn.
user253751

@MooingDuck: Phát hiện tốt. Tôi đã sửa nó bây giờ, tôi nghĩ. Cảm ơn!
Ilmari Karonen

@IlmariKaronen: Thật ra, nhìn vào mã, nó có thể đơn giản hơn chỉ để while(lastTime+=updateInterval <= time). Đó chỉ là một suy nghĩ, không phải là một sự điều chỉnh.
Vịt Mooing

7

Mã của bạn hiện đang chạy mỗi khi khung hình hiển thị. Nếu tốc độ khung hình cao hơn hoặc thấp hơn tốc độ khung hình đã chỉ định, kết quả của bạn sẽ thay đổi do các bản cập nhật không có cùng thời gian.

Để giải quyết điều này, bạn nên tham khảo Delta Timing .

Mục đích của Delta Timing là loại bỏ các ảnh hưởng của độ trễ đối với các máy tính cố xử lý đồ họa phức tạp hoặc nhiều mã, bằng cách thêm tốc độ của các vật thể để cuối cùng chúng sẽ di chuyển với cùng tốc độ, bất kể độ trễ.

Để làm điều này:

Nó được thực hiện bằng cách gọi một bộ đếm thời gian mỗi khung hình mỗi giây giữ thời gian từ bây giờ đến cuộc gọi cuối cùng tính bằng mili giây.

Sau đó, bạn sẽ cần nhân thời gian delta với giá trị bạn muốn thay đổi theo thời gian. Ví dụ:

distanceTravelledSinceLastFrame = Speed * DeltaTime

3
Ngoài ra, đặt mũ trên mức tối thiểu và tối đa. Nếu máy tính ngủ đông sau đó hoạt động trở lại, bạn không muốn mọi thứ khởi chạy khỏi màn hình. Nếu một phép lạ xuất hiện và time()trả về cùng hai lần, bạn không muốn lỗi div / 0 và lãng phí xử lý.
Vịt Mooing

@MooingDuck: Đó là một điểm rất tốt. Tôi đã chỉnh sửa câu trả lời của riêng mình để phản ánh nó. (Thông thường, bạn không nên chia bất cứ thứ gì cho dấu thời gian trong bản cập nhật trạng thái trò chơi thông thường, do đó, dấu thời gian bằng 0 phải an toàn, nhưng cho phép nó thêm một nguồn lỗi tiềm ẩn cho ít hoặc không thu được, và do đó nên tránh.)
Ilmari Karonen

5

Đó là bởi vì bạn giới hạn tốc độ khung hình của mình, nhưng bạn chỉ thực hiện một cập nhật cho mỗi khung hình. Vì vậy, giả sử trò chơi chạy ở mục tiêu 60 khung hình / giây, bạn nhận được 60 cập nhật logic mỗi giây. Nếu tốc độ khung hình giảm xuống 15 khung hình / giây, bạn chỉ có 15 cập nhật logic mỗi giây.

Thay vào đó, hãy thử tích lũy thời gian khung hình cho đến nay và sau đó cập nhật logic trò chơi của bạn một lần cho mỗi khoảng thời gian nhất định đã trôi qua, ví dụ: để chạy logic của bạn ở tốc độ 100 khung hình / giây, bạn sẽ chạy cập nhật một lần cho mỗi 10 ms tích lũy (và trừ đi quầy tính tiền).

Thêm một thay thế (tốt hơn cho hình ảnh) cập nhật logic của bạn dựa trên thời gian trôi qua.


1
tức là cập nhật (trôi qua giây);
Jon

2
Và bên trong, vị trí + = vận tốc * trôi qua giây;
Jon
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.