【问题标题】:Java rendering loop and logic loopJava渲染循环和逻辑循环
【发布时间】:2013-04-28 04:28:12
【问题描述】:

我一直在开发一款通用的 2d 瓷砖风格游戏。目前在我的主线程中,我有一个尽可能快的循环,并调用另一个处理游戏内容的类中的 JPanel 的重绘方法。它看起来像这样:

public class Main {
    public static void main(String[] args) {
        Island test = new Island();
        while(true) {
            test.getCanvas().requestFocus();
            test.getCanvas().repaint();
        }
    }
}

getCanvas() 只返回 JPanel。

目前这已经完成了我想要的,现在我已经开始为一名球员添加移动,显然我不想尽可能快地让他在屏幕上移动。所以我的 Island 类中有一个输入图和动作图,它检测按键的按下和释放,并告诉我的玩家类正在持有哪些键。然后,我使用每 10 毫秒调用一次的挥杆计时器在玩家类中移动我的玩家。所以我想这就像我的游戏滴答声,我的游戏将尽可能快地制作帧,然后游戏每秒执行 100 次所有逻辑内容,我当然会在游戏逻辑中添加更多内容,而不仅仅是运动。

在做了一些研究之后,似乎摇摆计时器并不是最好的方法,因为它是为执行小任务和摇摆任务而设计的。所以我想我的问题是,按照我的主要方法制作我的帧是否明智,以及让我的游戏每 10 毫秒可靠地运行一次的好方法是什么?我有一些想法,比如也许我应该创建一个新线程来处理游戏逻辑并使用 System.getnanotime 或任何它调用的东西来测量做一个滴答所需的时间,然后做一个小线程.sleep很长,直到我们达到 10 毫秒,然后重复。

如果您愿意,我很乐意发布更多代码 :),提前致谢。

【问题讨论】:

    标签: java multithreading loops time


    【解决方案1】:

    执行此操作的标准方法是在线程中。这是一个标准的游戏准系统游戏线程

    public class GameThread implements Runnable {
        private Thread runThread;
        private boolean running = false;
        private boolean paused = false;
        public GameThread() {
        }
    
        public void start() {
            running = true;
            paused = false;
            if(runThread == null || !runThread.isAlive())
                runThread = new Thread(this);
            else if(runThread.isAlive())
                throw new IllegalStateException("Thread already started.");
            runThread.start();
        }
    
        public void stop() {
            if(runThread == null)
                throw new IllegalStateException("Thread not started.");
            synchronized (runThread) {
                try {
                    running = false;
                    runThread.notify();
                    runThread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public void pause() {
            if(runThread == null)
                throw new IllegalStateException("Thread not started.");
            synchronized (runThread) {
                paused = true;
            }
        }
    
        public void resume() {
            if(runThread == null)
                throw new IllegalStateException("Thread not started.");
            synchronized (runThread) {
                paused = false;
                runThread.notify();
            }
        }
    
        public void run() {
            long sleep = 0, before;
            while(running) {
                // get the time before we do our game logic
                before = System.currentTimeMillis();
                // move player and do all game logic
                try {
                    // sleep for 100 - how long it took us to do our game logic
                    sleep = 100-(System.currentTimeMillis()-before);
                    Thread.sleep(sleep > 0 ? sleep : 0);
                } catch (InterruptedException ex) {
                }
                synchronized (runThread) {
                    if(paused) {
                        try {
                            runThread.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            paused = false;
        }
    }
    

    请注意,您需要调用 gameThead.start() 来开始您的游戏!

    【讨论】:

    • 这看起来正是我所追求的!我刚刚完成了它的实现,但是它并没有完全按预期工作。我认为它只是运行得非常快,所以我只是缩放地图,我所拥有的是基本的,所以我还没有检测到地图的边缘。我会在更多调整后尽快回复,但现在谢谢你! :)
    • 只是一个简单的问题,不应该是 runThread.sleep() 还是现在它是这样工作的?
    • Thread.sleep 是一个静态方法,它作用于执行那行代码的任何线程。
    • 谢谢,我已经解决了我的问题,现在它可以完美运行了,我必须执行“sleep = 100-(System.currentTimeMillis()-before);”让它工作
    • @Iain 不要忘记检查 System.currentTimeMillis()-before 是否大于 100
    【解决方案2】:

    在不消耗 100% CPU 的情况下创建一个好的(窗口应用程序)游戏循环实际上是一项非常棘手的任务。 Sidescroll 以恒定速度为 sprite 设置动画很容易引入急动,如果可以看到的话。

    在运行了几个想法之后,这是最好的,横向滚动大部分时间都是黄油光滑的。 VSYNCing 是一种在窗口模式下是否有效的东西,我在不同的机器和操作系统上发现了不同的结果。

    测试应用没有使用 SwingUI,因为大多数游戏都不需要它。 Gameloop 是一个活跃的更新-渲染循环,没有外部线程,使事情更容易编程。使用 keyPressed 回调更新 firePressed=true 等 标志变量并在循环中使用值。

    运行测试应用 c:> java -cp ./classes GameLoop2 "fullscreen=false" "fps=60" "vsync=true"

    //http://www.javagaming.org/index.php/topic,19971.0.html
    //http://fivedots.coe.psu.ac.th/~ad/jg/ch1/ch1.pdf
    
    import java.util.*;
    
    import java.awt.Color;
    import java.awt.Frame;
    import java.awt.Graphics;
    import java.awt.GraphicsConfiguration;
    import java.awt.GraphicsDevice;
    import java.awt.GraphicsEnvironment;
    import java.awt.Toolkit;
    import java.awt.event.KeyEvent;
    import java.awt.event.KeyListener;
    import java.awt.geom.Rectangle2D;
    import java.awt.image.BufferStrategy;
    
    import java.awt.DisplayMode; // for full-screen mode
    
    public class GameLoop2 implements KeyListener {
        Frame mainFrame;
    
        private static final long NANO_IN_MILLI = 1000000L; 
    
        // num of iterations with a sleep delay of 0ms before
        // game loop yields to other threads.
        private static final int NO_DELAYS_PER_YIELD = 16;
    
        // max num of renderings that can be skipped in one game loop,
        // game's internal state is updated but not rendered on screen.
        private static int MAX_RENDER_SKIPS = 5;
    
        private static int TARGET_FPS = 60;
    
        //private long prevStatsTime;
        private long gameStartTime;
        private long curRenderTime;
        private long rendersSkipped = 0L;
        private long period; // period between rendering in nanosecs
    
        long fps;
        long frameCounter;
        long lastFpsTime;
    
        Rectangle2D rect, rect2, rect3;
    
        /**
         * Create a new GameLoop that will use the specified GraphicsDevice.
         * 
         * @param device
         */
        public GameLoop2(Map<String,String> args, GraphicsDevice device) {
            try {
                if (args.containsKey("fps"))
                  TARGET_FPS = Integer.parseInt(args.get("fps"));
    
    
                // Setup the frame
                GraphicsConfiguration gc = device.getDefaultConfiguration();
    
                mainFrame = new Frame(gc);
                //mainFrame.setUndecorated(true);
                mainFrame.setIgnoreRepaint(true);
                mainFrame.setVisible(true);
                mainFrame.setSize(640, 480);
                //mainFrame.setLocationRelativeTo();
                mainFrame.setLocation(700,100);
                mainFrame.createBufferStrategy(2);
                mainFrame.addKeyListener(this);
    
                if ("true".equalsIgnoreCase(args.get("fullscreen"))) {
                  device.setFullScreenWindow(mainFrame);
                  device.setDisplayMode(new DisplayMode(640, 480, 8, DisplayMode.REFRESH_RATE_UNKNOWN));
                }
    
                final boolean VSYNC = "true".equalsIgnoreCase(args.get("vsync"));
    
                // Cache the buffer strategy and create a rectangle to move
                BufferStrategy bufferStrategy = mainFrame.getBufferStrategy();
    
                rect = new Rectangle2D.Float(0,100, 64,64);
                rect2 = new Rectangle2D.Float(0,200, 32,32);
                rect3 = new Rectangle2D.Float(500,300, 128,128);
    
                // loop initialization
                long beforeTime, afterTime, timeDiff, sleepTime;
                long overSleepTime = 0L;
                int noDelays = 0;
                long excess = 0L;
                gameStartTime = System.nanoTime();
                //prevStatsTime = gameStartTime;
                beforeTime = gameStartTime;
    
                period = (1000L*NANO_IN_MILLI)/TARGET_FPS;  // rendering FPS (nanosecs/targetFPS)
                System.out.println("FPS: " + TARGET_FPS + ", vsync=" + VSYNC);
                System.out.println("FPS period: " + period);
    
    
                // Main loop
                while(true) {
                   // **2) execute physics
                   updateWorld(0);                  
    
                   // **1) execute drawing
                   Graphics g = bufferStrategy.getDrawGraphics();
                   drawScreen(g);
                   g.dispose();
    
                   // Synchronise with the display hardware. Note that on
                   // Windows Vista this method may cause your screen to flash.
                   // If that bothers you, just comment it out.
                   if (VSYNC) Toolkit.getDefaultToolkit().sync();
    
                   // Flip the buffer
                   if( !bufferStrategy.contentsLost() )
                       bufferStrategy.show();
    
                   afterTime = System.nanoTime();
                   curRenderTime = afterTime;
                   calculateFramesPerSecond();
    
                   timeDiff = afterTime - beforeTime;
                   sleepTime = (period-timeDiff) - overSleepTime;
                   if (sleepTime > 0) { // time left in cycle
                      //System.out.println("sleepTime: " + (sleepTime/NANO_IN_MILLI));
                      try {
                         Thread.sleep(sleepTime/NANO_IN_MILLI);//nano->ms
                      } catch(InterruptedException ex){}
                      overSleepTime = (System.nanoTime()-afterTime) - sleepTime;
                   } else { // sleepTime <= 0;
                      System.out.println("Rendering too slow");
                      // this cycle took longer than period
                      excess -= sleepTime;
                      // store excess time value
                      overSleepTime = 0L;
                      if (++noDelays >= NO_DELAYS_PER_YIELD) {
                         Thread.yield();
                         // give another thread a chance to run
                         noDelays = 0;
                      }
                   }
    
                   beforeTime = System.nanoTime();
    
                   /* If the rendering is taking too long, then
                      update the game state without rendering
                      it, to get the UPS nearer to the
                      required frame rate. */
                   int skips = 0;
                   while((excess > period) && (skips < MAX_RENDER_SKIPS)) {
                      // update state but don’t render
                      System.out.println("Skip renderFPS, run updateFPS");
                      excess -= period;
                      updateWorld(0);
                      skips++;
                   }
                   rendersSkipped += skips;
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                device.setFullScreenWindow(null);
            }
        }
    
        private void updateWorld(long elapsedTime) {
            // speed: 150 pixels per second
            //double xMov = (140f/(NANO_IN_MILLI*1000)) * elapsedTime;
            double posX = rect.getX() + (140f / TARGET_FPS);
        if (posX > mainFrame.getWidth())
            posX = -rect.getWidth();    
        rect.setRect(posX, rect.getY(), rect.getWidth(), rect.getHeight());
    
            posX = rect2.getX() + (190f / TARGET_FPS);
        if (posX > mainFrame.getWidth())
            posX = -rect2.getWidth();   
        rect2.setRect(posX, rect2.getY(), rect2.getWidth(), rect2.getHeight());         
    
            posX = rect3.getX() - (300f / TARGET_FPS);
        if (posX < -rect3.getWidth())
            posX = mainFrame.getWidth();
        rect3.setRect(posX, rect3.getY(), rect3.getWidth(), rect3.getHeight());         
    
        }
    
        private void drawScreen(Graphics g) {
            g.setColor(Color.BLACK);
            g.fillRect(0, 0, mainFrame.getWidth(), mainFrame.getHeight());
            g.setColor(Color.WHITE);
            g.drawString("FPS: " + fps, 40, 50);
    
            g.setColor(Color.RED);
            g.fillRect((int)rect.getX(), (int)rect.getY(), (int)rect.getWidth(), (int)rect.getHeight());
    
            g.setColor(Color.GREEN);
            g.fillRect((int)rect2.getX(), (int)rect2.getY(), (int)rect2.getWidth(), (int)rect2.getHeight());
    
            g.setColor(Color.BLUE);
            g.fillRect((int)rect3.getX(), (int)rect3.getY(), (int)rect3.getWidth(), (int)rect3.getHeight());
        }
    
        private void calculateFramesPerSecond() {
            if( curRenderTime - lastFpsTime >= NANO_IN_MILLI*1000 ) {
                fps = frameCounter;
                frameCounter = 0;
                lastFpsTime = curRenderTime;
            }
            frameCounter++;
        }
    
        public void keyPressed(KeyEvent e) {
            if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
                System.exit(0);
            }
        }
    
        public void keyReleased(KeyEvent e) { }
        public void keyTyped(KeyEvent e) { }
    
        public static void main(String[] args) {
            try {
            Map<String,String> mapArgs = parseArguments(args);
    
                GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
                GraphicsDevice device = env.getDefaultScreenDevice();
                new GameLoop2(mapArgs, device);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    
    
        /**
         * Parse commandline arguments, each parameter is a name-value pair.
         * Example: java.exe MyApp "key1=value1" "key2=value2"
         */
        private static Map<String,String> parseArguments(String[] args) {
            Map<String,String> mapArgs = new HashMap<String,String>();
    
            for(int idx=0; idx < args.length; idx++) {
                String val = args[idx];
                int delimIdx = val.indexOf('=');
                if (delimIdx < 0) {
                    mapArgs.put(val, null);
                } else if (delimIdx == 0) {
                    mapArgs.put("", val.substring(1));
                } else {
                    mapArgs.put(
                        val.substring(0, delimIdx).trim(),
                        val.substring(delimIdx+1)
                    );
                }
            }
    
            return mapArgs;
        }
    
    }
    

    Gameloop 逻辑可能看起来很疯狂,但相信我,它运行得非常好。试一试。 编辑: 同时运行 TaskManager 会在流畅的动画中产生抖动我猜更新检测统计数据会带来沉重的打击。

    【讨论】:

    • 这和我去年夏天写的一些代码很相似。虽然,这似乎是它的一个紧凑版本,我记得在睡眠和保持 updates-per-secondframes-per-second 同步时遇到了一些问题,当具有相同的目标率时。在未来的某个时候,我可能会从这里借鉴一些想法。
    • 对不起,我刚才才注意到你的回答。现在已经很晚了,所以我明天会更好地看看它。我非常感谢您的意见!然而,我注意到的一件事是它具有固定的 FPS,我认为尽可能多地制作 fps 并以设定的时间间隔执行游戏逻辑可能会更好,这样我的游戏就可以在更多种类的机器上运行?不管怎样,谢谢:)
    • 我不是专家,游戏循环是一个非常复杂的问题,当物理学发挥作用时更是如此。我希望看到替代(Java)解决方案,其中三个精灵像我的示例一样向左向右移动。有关更多信息,请参阅 gafferongames.com/game-physics/fix-your-timestepgamedev.stackexchange.com/questions/1589/… 讨论。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-21
    • 2017-05-05
    • 1970-01-01
    相关资源
    最近更新 更多