2009-04-21 82 views
13

Tôi đang viết một trò chơi đơn giản, và tôi muốn giới hạn tốc độ khung hình của tôi ở tốc độ 60 khung hình/giây mà không làm cho vòng lặp ăn cpu của tôi. Làm thế nào tôi sẽ làm điều này?Làm cách nào để tăng tốc độ khung hình của tôi ở tốc độ 60 khung hình/giây trong Java?

+4

Không mã hóa 60 FPS, xác định tốc độ làm mới thực tế của màn hình. 60 FPS sẽ bị giật, ví dụ: một màn hình 85 Hz. –

Trả lời

32

Bạn có thể đọc số Game Loop Article. Điều quan trọng là trước tiên bạn phải hiểu các phương pháp khác nhau cho vòng lặp trò chơi trước khi cố gắng triển khai bất kỳ điều gì.

+0

Dễ hiểu bài viết. – mfx

5

Câu trả lời đơn giản là đặt java.util.Timer để kích hoạt mỗi 17 ms và thực hiện công việc của bạn trong sự kiện hẹn giờ.

+1

Điều gì sẽ xảy ra nếu quá 17ms để hoàn thành công việc trong sự kiện hẹn giờ? – cherouvim

+0

@cherouvim: Sau đó, bạn bị quá tải và tốc độ khung hình giảm xuống. Bạn vẫn đang làm việc nhanh nhất có thể, nhưng không nhanh hơn 60 khung hình/giây. –

4

Đây là cách tôi đã làm trong C++ ... Tôi chắc rằng bạn có thể điều chỉnh nó.

void capFrameRate(double fps) { 
    static double start = 0, diff, wait; 
    wait = 1/fps; 
    diff = glfwGetTime() - start; 
    if (diff < wait) { 
     glfwSleep(wait - diff); 
    } 
    start = glfwGetTime(); 
} 

Chỉ cần gọi với capFrameRate(60) một lần trên mỗi vòng lặp. Nó sẽ ngủ, vì vậy nó không lãng phí chu kỳ quý giá. glfwGetTime() trả về thời gian kể từ khi chương trình bắt đầu tính bằng giây ... Tôi chắc rằng bạn có thể tìm thấy tương đương trong Java ở đâu đó.

+1

Trong Java, tương đương với glfwGetTime() sẽ là System.currentTimeMillis(). Nó trả về thời gian tính bằng mili giây kể từ ngày 1 tháng 1 năm 1970. Khi được sử dụng như thế này, nó thực hiện tương tự, ngoại trừ nó trả về thời gian bằng mili giây. – Manitobahhh

0

Trong java bạn có thể làm System.currentTimeMillis() để nhận thời gian bằng mili giây thay vì glfwGetTime().

Thread.sleep(time in milliseconds) làm cho chuỗi chờ trong trường hợp bạn không biết và nó phải nằm trong khối try.

0

Điều tôi đã làm là tiếp tục lặp lại và theo dõi thời điểm tôi thực hiện hoạt ảnh lần cuối. Nếu nó đã được ít nhất 17 ms sau đó đi qua các chuỗi hình ảnh động.

Bằng cách này tôi có thể kiểm tra mọi đầu vào của người dùng và bật/tắt ghi chú nhạc nếu cần.

Nhưng, đây là một chương trình giúp dạy nhạc cho trẻ em và ứng dụng của tôi đang cài đặt máy tính ở chế độ toàn màn hình, vì vậy nó không hoạt động tốt với người khác.

0

Nếu bạn đang sử dụng Java Swing cách đơn giản nhất để đạt được một fps 60 là bằng cách thiết lập một javax.swing.Timer như thế này và là một cách phổ biến để làm cho trò chơi:

public static int DELAY = 1000/60; 

Timer timer = new Timer(DELAY,new ActionListener() { 
    public void actionPerformed(ActionEvent event) 
    { 
     updateModel(); 
     repaintScreen(); 
    } 
}); 

Và đâu đó trong bạn mã bạn phải thiết lập bộ đếm thời gian này để lặp lại và bắt đầu nó:

timer.setRepeats(true); 
timer.start(); 

Mỗi giây phút có 1.000 ms và bằng cách chia này với fps của bạn (60) và thiết lập bộ đếm thời gian với sự chậm trễ này (1000-1060 = 16 ms tròn xuống) bạn sẽ nhận được một tốc độ khung hình cố định. Tôi nói "hơi" bởi vì điều này phụ thuộc rất nhiều vào những gì bạn làm trong các cuộc gọi updateModel() và repaintScreen() ở trên.

Để nhận kết quả dự đoán được nhiều hơn, hãy thử thời gian hai cuộc gọi trong bộ hẹn giờ và đảm bảo kết thúc trong vòng 16 mili giây để duy trì 60 khung hình/giây. Với preallocation thông minh của bộ nhớ, tái sử dụng đối tượng và những thứ khác bạn có thể làm giảm tác động của Garbage Collector cũng. Nhưng đó là một câu hỏi khác.

+0

Sự cố với bộ hẹn giờ có thể so sánh được với việc sử dụng Thread.Sleep ... không đảm bảo độ trễ chính xác. Nói chung, DELAY thực tế là không thể đoán trước và có thể dài hơn một chút so với thông số DELAY của bạn. –

1

Bộ đếm thời gian không chính xác để điều khiển FPS trong java. Tôi đã tìm thấy rằng trong tay thứ hai. Bạn sẽ cần phải thực hiện bộ đếm thời gian của riêng bạn hoặc làm FPS thời gian thực với những hạn chế. Nhưng không sử dụng Timer vì nó không chính xác 100%, và do đó không thể thực thi các tác vụ đúng cách.

7

Tôi đã Game Loop Article rằng @cherouvim được đăng, và tôi đã chiến lược "tốt nhất" và cố gắng viết lại nó cho một java Runnable, dường như được làm việc cho tôi

double interpolation = 0; 
final int TICKS_PER_SECOND = 25; 
final int SKIP_TICKS = 1000/TICKS_PER_SECOND; 
final int MAX_FRAMESKIP = 5; 

@Override 
public void run() { 
    double next_game_tick = System.currentTimeMillis(); 
    int loops; 

    while (true) { 
     loops = 0; 
     while (System.currentTimeMillis() > next_game_tick 
       && loops < MAX_FRAMESKIP) { 

      update_game(); 

      next_game_tick += SKIP_TICKS; 
      loops++; 
     } 

     interpolation = (System.currentTimeMillis() + SKIP_TICKS - next_game_tick 
       /(double) SKIP_TICKS); 
     display_game(interpolation); 
    } 
} 
+0

Nó là bộ xương của cách tốt hơn để làm một vòng lặp trò chơi, nhưng nó không giải thích nhiều không? Có thể thêm một vài điểm đạn ngắn giải thích mọi thứ? –

+0

Đây là cách tôi làm điều đó. Ok, vì vậy chúng tôi lặp lại, và nếu thời gian qua giữa các vòng lặp lặp lại ít hơn bỏ qua, chúng tôi chỉ lặp lại. Nếu bỏ qua hoặc nhiều thời gian trôi qua, chúng tôi sẽ gọi công cụ trò chơi chính và yêu cầu khung tiếp theo. Tôi thích kéo toàn bộ khung hình thành hình ảnh và gửi cho phương pháp vẽ. Trong lớp trò chơi của tôi, tôi muốn tính toán khung hình tối đa mỗi giây trong trường hợp người chơi muốn hiển thị FPS thực, khi tôi đặt tối đa fps, thực tế luôn là một vài khung nhỏ hơn số lượng đã đặt, tức là đặt ở 60 thực tế là 57. –

2

Đây là một mẹo để sử dụng Swing và nhận được FPS chính xác hợp lý mà không cần sử dụng Bộ hẹn giờ.

Trước hết không chạy vòng lặp trò chơi trong chuỗi điều phối sự kiện, nó sẽ chạy trong chuỗi của riêng nó làm việc chăm chỉ và không cản trở giao diện người dùng.

Các thành phần Swing hiển thị các trò chơi nên vẽ tự trọng phương pháp này:

@Override 
public void paintComponent(Graphics g){ 
    // draw stuff 
} 

Việc nắm bắt được rằng trò chơi vòng lặp không biết khi nào event dispatcher đã thực sự có vòng để thực hiện hành động vì sơn trong vòng lặp trò chơi, chúng ta chỉ cần gọi component.repaint() yêu cầu chỉ vẽ mà có thể xảy ra sau đó.

Sử dụng chờ/thông báo cho bạn có thể lấy lại phương pháp vẽ lại cho đến khi sơn được yêu cầu xảy ra và sau đó tiếp tục.

Dưới đây là hoàn chỉnh mã làm việc từ https://github.com/davidmoten/game-exploration mà không một chuyển động đơn giản của một chuỗi trên một JFrame sử dụng các phương pháp trên:

import java.awt.BorderLayout; 
import java.awt.Component; 
import java.awt.Graphics; 
import java.awt.Image; 

import javax.swing.JFrame; 
import javax.swing.JPanel; 

/** 
* Self contained demo swing game for stackoverflow frame rate question. 
* 
* @author dave 
* 
*/ 
public class MyGame { 

    private static final long REFRESH_INTERVAL_MS = 17; 
    private final Object redrawLock = new Object(); 
    private Component component; 
    private volatile boolean keepGoing = true; 
    private Image imageBuffer; 

    public void start(Component component) { 
     this.component = component; 
     imageBuffer = component.createImage(component.getWidth(), 
       component.getHeight()); 
     Thread thread = new Thread(new Runnable() { 
      @Override 
      public void run() { 
       runGameLoop(); 
      } 
     }); 
     thread.start(); 
    } 

    public void stop() { 
     keepGoing = false; 
    } 

    private void runGameLoop() { 
     // update the game repeatedly 
     while (keepGoing) { 
      long durationMs = redraw(); 
      try { 
       Thread.sleep(Math.max(0, REFRESH_INTERVAL_MS - durationMs)); 
      } catch (InterruptedException e) { 
      } 
     } 
    } 

    private long redraw() { 

     long t = System.currentTimeMillis(); 

     // At this point perform changes to the model that the component will 
     // redraw 

     updateModel(); 

     // draw the model state to a buffered image which will get 
     // painted by component.paint(). 
     drawModelToImageBuffer(); 

     // asynchronously signals the paint to happen in the awt event 
     // dispatcher thread 
     component.repaint(); 

     // use a lock here that is only released once the paintComponent 
     // has happened so that we know exactly when the paint was completed and 
     // thus know how long to pause till the next redraw. 
     waitForPaint(); 

     // return time taken to do redraw in ms 
     return System.currentTimeMillis() - t; 
    } 

    private void updateModel() { 
     // do stuff here to the model based on time 
    } 

    private void drawModelToImageBuffer() { 
     drawModel(imageBuffer.getGraphics()); 
    } 

    private int x = 50; 
    private int y = 50; 

    private void drawModel(Graphics g) { 
     g.setColor(component.getBackground()); 
     g.fillRect(0, 0, component.getWidth(), component.getHeight()); 
     g.setColor(component.getForeground()); 
     g.drawString("Hi", x++, y++); 
    } 

    private void waitForPaint() { 
     try { 
      synchronized (redrawLock) { 
       redrawLock.wait(); 
      } 
     } catch (InterruptedException e) { 
     } 
    } 

    private void resume() { 
     synchronized (redrawLock) { 
      redrawLock.notify(); 
     } 
    } 

    public void paint(Graphics g) { 
     // paint the buffered image to the graphics 
     g.drawImage(imageBuffer, 0, 0, component); 

     // resume the game loop 
     resume(); 
    } 

    public static class MyComponent extends JPanel { 

     private final MyGame game; 

     public MyComponent(MyGame game) { 
      this.game = game; 
     } 

     @Override 
     protected void paintComponent(Graphics g) { 
      game.paint(g); 
     } 
    } 

    public static void main(String[] args) { 
     java.awt.EventQueue.invokeLater(new Runnable() { 
      @Override 
      public void run() { 
       MyGame game = new MyGame(); 
       MyComponent component = new MyComponent(game); 

       JFrame frame = new JFrame(); 
       // put the component in a frame 

       frame.setTitle("Demo"); 
       frame.setSize(800, 600); 

       frame.setLayout(new BorderLayout()); 
       frame.getContentPane().add(component, BorderLayout.CENTER); 
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
       frame.setVisible(true); 

       game.start(component); 
      } 
     }); 
    } 

} 
+0

Tôi nghĩ rằng bất kỳ vòng lặp trò chơi nào sử dụng [Thread.Sleep] (https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#sleep%28long,%20int%29) là thiếu sót . Từ docu: 'Gây chủ đề hiện đang thực hiện cho giấc ngủ [...] cho số mili giây được chỉ định cộng với số nano giây được chỉ định, ** tùy thuộc vào độ chính xác và độ chính xác của bộ hẹn giờ hệ thống và lịch trình **. Không có gì đảm bảo rằng chuỗi sẽ thực sự ngủ cho số lượng mili giây + nanos bạn yêu cầu. –

+0

Tôi đồng ý rằng việc sử dụng 'Thread.sleep' không phải là lý tưởng. Kỹ thuật không chặn sẽ đẹp hơn.Tôi không chắc rằng bạn có thể làm tốt hơn nữa mà độ chính xác 'Thread.sleep' cung cấp với một kỹ thuật không chặn mặc dù sử dụng câu lệnh' ScheduledExecutorService'. Bạn nghĩ sao? –

+0

IMO là tốt nhất là một vòng lặp tương tự như một được đăng bởi Parth Mehrotra, tức là một trong đó không dựa vào ngủ hoặc một cái gì đó tương tự. –

0

Có rất nhiều thông tin tốt ở đây đã có, nhưng tôi muốn chia sẻ một vấn đề mà tôi đã có với các giải pháp này và giải pháp cho điều đó. Nếu, mặc dù tất cả các giải pháp này, trò chơi/cửa sổ của bạn dường như bị bỏ qua (đặc biệt là dưới X11), hãy thử gọi Toolkit.getDefaultToolkit().sync() một lần trên mỗi khung hình.

Các vấn đề liên quan