简体   繁体   中英

Java rendering loop and logic loop

I've been working on a generic 2d tile style game. Currently In my main thread I have a loop that just goes as fast as it possibly can and calls the repaint method of a JPanel that is in another class which handles the game stuff. It looks like this:

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

getCanvas() just returns the JPanel.

Currently this has done what I've wanted, now that I've started adding movement for a player and obviously I don't want to move him across the screen as fast as I possibly can. So I have an input map and action map in my Island class and that detects the key presses and releases and tells my player class which keys are being held. I then move my player about inside the player class with a swing timer that calls every 10ms. So I guess this is like my game tick, my game will make frames as fast as it possibly can and then the game does all its logic stuff 100 times a second, I will be adding more to the gamelogic of course, not just movement.

After doing some research it would appear a swing timer isn't the best way of doing this as it is designed for doing small tasks and for swing tasks. So I guess my question is, is it sensible to make my frames the way I am in my main method, and what would be a good way of getting my game to tick reliably every 10ms or whatever? I've had a few ideas, like maybe I should make a new thread that handles the game logic and use the System.getnanotime or whatever its called to measure the time taken to do a tick and then do a wee thread.sleep for however long it is until we reach 10ms and then repeat.

I am happy to post more code if you want :), and thanks in advance.

A standard way of doing this is in a Thread. Heres a standard game barebones game thread

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

Note you will need to call gameThead.start() to start your game!

Creating a good (windowed app) gameloop without eating CPU 100% is actually a very tricky task. Sidescroll animating a sprite with constant speed easily introduces jerkiness if there is one to be seen.

After running several ideas this is what found best, sidescrolling is most of the time butter smooth. VSYNCing is something what may work good in a windowed mode or not, I've found varying results on different machines and OSses.

Test app is not using SwingUI because most games don't need it anyway. Gameloop is an active update-render loop without external threads making things easier to program. Use keyPressed callback to update firePressed=true and etc flag variables and use values in a loop.

Run test app 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 logic may look crazy but believe me this works suprisingly well. Give it a run. edit: Running TaskManager at the same time creates jerkiness in a smooth animation I guess updating instrumentation statistics gives a heavy hit.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM