Java基础2d游戏动画口吃

所以,我已经有一段时间在一个2d RPG上工作,我似乎无法解决这个问题。 由于未知原因,图形似乎每隔几秒就会“跳跃”或断断续续。 这变得非常烦人,因为我不知道是什么导致它。

这是我写的一个非常基本的程序,它只有一个红色方块,从屏幕的一侧移动到另一侧。 即使在这个非常基本的程序中,广场仍然会断断每一次更新,我真的无法理解我的生活。

public class Main extends JPanel { int x=0, y=0; public JFrame window = new JFrame("Window"); public Main(){ window.setSize(1000, 500); window.setVisible(true); window.add(this); } public void paintComponent(Graphics g){ super.paintComponent(g); g.setColor(Color.red); g.fillRect(x, y, 500, 500); x+=3; if(x>900){ x=0; } } public void start(){ while(true){ repaint(); try { Thread.sleep(16); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args){ Main game = new Main(); game.start(); } } 

如果您运行该类,您将以图形方式查看问题所在。 显然我的游戏由更多的类组成,并且比这复杂得多,但同样的原则适用。 如果有人对我的问题有任何见解,我很乐意听到。 提前致谢。

更新

这是我的两个主要课程:

主类:
包com.ultimatum.Main;

 import java.awt.Dimension; import java.awt.Graphics; import java.awt.Toolkit; import java.awt.image.BufferedImage; import javax.swing.JFrame; import javax.swing.JPanel; import com.ultimatum.Mangers.ImageLoader; import com.ultimatum.Mangers.KeyStates; import com.ultimatum.Mangers.ScreenUpdater; import com.ultimatum.Mangers.UserInput; @SuppressWarnings("serial") public class Ultimatum extends JPanel { /** * @param x This is the start width of the screen and can be adjusted manually, but will not go any lower than this integer * @param y This is the start height of the screen and can be adjusted manually, but will not go any lower than this integer * @param contentPlaneX This is how much the width of the content plane is (Frame-Borders) * @param contentPlaneY This is how much the height of the content plane is (Frame-Borders) */ public int x=850, y=610, contentPlaneX, contentPlaneY, middleX, middleY, tileSize=90; public Dimension minimumSize = new Dimension(x, y); public JFrame window = new JFrame("Ultimatum");//This makes the JFrame for the game public KeyStates keyStates = new KeyStates(); public UserInput input = new UserInput(keyStates); public ImageLoader imageLoader = new ImageLoader(); public static Ultimatum ultimatum;//Makes the object of this class public static ScreenUpdater su;//This is creating the object that is going to be making changes to the screen. For example, the animation. private BufferedImage screenImage; public boolean isWindowInFullscreenMode; private boolean imagesLoaded; public void initializeUltimatum(){ toWindowedMode(); addMouseListener(input); addMouseMotionListener(input); contentPlaneX=window.getContentPane().getWidth(); contentPlaneY=window.getContentPane().getHeight(); middleX=(int)contentPlaneX/2; middleY=(int)contentPlaneY/2; su = new ScreenUpdater(ultimatum, keyStates, imageLoader, "Test", tileSize); imageLoader.loadImages(); } public void toFullscreenMode(){ window.dispose(); window.setUndecorated(true); window.setVisible(true); window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); window.setBounds(0,0,Toolkit.getDefaultToolkit().getScreenSize().width,Toolkit.getDefaultToolkit().getScreenSize().height); addListenersAndClassToWindow(); isWindowInFullscreenMode=true; } public void toWindowedMode(){ window.dispose(); window.setUndecorated(false); window.setSize(x,y); window.setMinimumSize(minimumSize); window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); window.setVisible(true); window.setLocationRelativeTo(null); addListenersAndClassToWindow(); isWindowInFullscreenMode=false; } public void addListenersAndClassToWindow(){ window.add(ultimatum);//This connects paintComponent and the frame to this class window.addKeyListener(input); } public void paintComponent(Graphics g){ if(imagesLoaded){ super.paintComponent(g); //su.updateScreen(g); g.drawImage(screenImage, 0, 0, contentPlaneX, contentPlaneY, null); }else imagesLoaded = true; } public void update(){ screenImage = su.updateScreen(); } /** * This main class sets up the program. The while loop that keeps the game running is also contained inside this class. Most of this class is easily * readable so i'm not going to comment that much. */ public static void main(String[] args){ ultimatum = new Ultimatum(); ultimatum.initializeUltimatum(); final int FPS=60, TARGET_TIME=1000/FPS; long start, elapsed, wait; while(true){//This loops purpose is to keep the game running smooth on all computers start = System.nanoTime(); ultimatum.update(); ultimatum.repaint();//This calls the paintComponent method elapsed = System.nanoTime() - start; wait = TARGET_TIME-elapsed/1000000; if(wait<0) wait = TARGET_TIME; try{//Catches the error in case the tries to give an error (which it won't) Thread.sleep(wait);//This is how long it waits it till the screen gets repainted }catch(Exception e){ e.printStackTrace(); } } } } 

屏幕更新器:

 package com.ultimatum.Mangers; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.image.BufferedImage; import com.ultimatum.Engine.BuildingGenerator; import com.ultimatum.Engine.TextBoxGenerator; import com.ultimatum.Entities.Character.Player; import com.ultimatum.Gamestates.Buildings.HealingCenter; import com.ultimatum.Gamestates.Menus.EscapeScreen; import com.ultimatum.Gamestates.Menus.StartScreen; import com.ultimatum.Gamestates.Menus.TitleScreen; import com.ultimatum.Gamestates.Routes.RouteSuperClass; import com.ultimatum.Gamestates.Towns.TownSuperClass; import com.ultimatum.Main.Ultimatum; public class ScreenUpdater { public Ultimatum ul; public Resizer rs;//This is the object that captures the resize in two integers public KeyStates ks; public ImageLoader loader; public Fader fader; public TextBoxGenerator textBox; public Initializer initer; public TileMap tm; public Player p; public BuildingGenerator bg; //Menus public TitleScreen titleScreen; public StartScreen startScreen; public EscapeScreen escScreen; //Towns public TownSuperClass towns; //Routes public RouteSuperClass routes; //Buildings public HealingCenter healingCenter; public final int TITLE_SCREEN=0, START_SCREEN=TITLE_SCREEN+1, LOAD=START_SCREEN+1, TOWN_ONE=LOAD+1, ROUTE_ONE=TOWN_ONE+1, HEALING_CENTER=ROUTE_ONE+1, ESC_MENU=HEALING_CENTER+1; public int screenNo = TITLE_SCREEN; public int prevScreen=0; public boolean prevMenuState, menuState;//These variables are for the checkEsc method public boolean isMouseVisible=true, prevIsMouseVisible;//Simple boolean for setting the mouse from visible to invisible and vice versa public ScreenUpdater(Ultimatum ultimatum, KeyStates keyStates, ImageLoader imageloader, String location, int tileSize){ ul = ultimatum; ks = keyStates; loader = imageloader; fader = new Fader(ul, this); textBox = new TextBoxGenerator(loader, ks, ul); initer = new Initializer(fader, textBox); fader.sendIniterData(initer); p = new Player(ul, fader, loader, ks, initer, this); fader.sendPlayerData(p); tm = new TileMap(tileSize, loader, p); fader.sendTileMapData(tm); rs = new Resizer(ul, p); bg = new BuildingGenerator(ul, p, loader, tm); //Below are the game states being loaded //Menus titleScreen = new TitleScreen(ul, this, loader, ks, fader); startScreen = new StartScreen(ul, this, fader, loader, ks, textBox); escScreen = new EscapeScreen(ul, fader, loader, ks); rs.sendEscapeScreenData(escScreen); //Towns towns = new TownSuperClass(p, fader, bg, tm, this); //Routes routes = new RouteSuperClass(p, fader, bg, tm, this); //Buildings healingCenter = new HealingCenter(ul, fader, loader, ks, textBox); } public void clearScreen(Graphics g){ g.setColor(Color.black); g.fillRect(0, 0, ul.contentPlaneX, ul.contentPlaneY); } public void checkEsc(Graphics g){ if(ks.escReleased&&screenNo>LOAD&&!fader.fadingOut&&fader.fadingIn){ if(screenNo<HEALING_CENTER&&!p.isMoving){ menuState=true; prevScreen=screenNo; } else if(screenNo==ESC_MENU) menuState=false; } if(prevMenuState!=menuState){ int toScreen; boolean mouseVisiblity; if(menuState){ toScreen=ESC_MENU; mouseVisiblity=true; } else{ toScreen=prevScreen; mouseVisiblity=false; } fader.FadeOut(g, 255, toScreen, false, "", 0, 0, false, 0, mouseVisiblity);//The zeros don't matter because the boolean is set to false if(!fader.fadingOut){ prevMenuState=menuState; initer.initFader(); } } } public void checkForF11(){ if(ks.isF11PressedThenReleased){ if(ul.isWindowInFullscreenMode) ul.toWindowedMode(); else ul.toFullscreenMode(); } } public void setMouseVisible(){ ul.window.setCursor(ul.window.getToolkit().createCustomCursor(loader.cursor, new Point(0, 0),"Visible")); } public void setMouseInvisble(){ ul.window.setCursor(ul.window.getToolkit().createCustomCursor(new BufferedImage(3, 3, BufferedImage.TYPE_INT_ARGB), new Point(0, 0),"Clear")); } public void checkMouseState(){ if(isMouseVisible!=prevIsMouseVisible){ if(isMouseVisible) setMouseVisible(); else setMouseInvisble(); prevIsMouseVisible=isMouseVisible; } } public BufferedImage updateScreen(){ BufferedImage screenImage = new BufferedImage(ul.contentPlaneX, ul.contentPlaneY, BufferedImage.TYPE_INT_ARGB); Graphics2D screenGraphics = screenImage.createGraphics(); Color oldColor = screenGraphics.getColor(); screenGraphics.setPaint(Color.white); screenGraphics.fillRect(0, 0, ul.contentPlaneX, ul.contentPlaneY); screenGraphics.setColor(oldColor); checkForF11(); clearScreen(screenGraphics); switch(screenNo){ case TITLE_SCREEN: titleScreen.titleScreen(screenGraphics); break; case START_SCREEN: startScreen.startScreen(screenGraphics); break; case TOWN_ONE: towns.townOne(screenGraphics); break; case ROUTE_ONE: routes.routeOne(screenGraphics); break; case HEALING_CENTER: healingCenter.healingCenter(screenGraphics); break; case ESC_MENU: escScreen.escapeScreen(screenGraphics); break; } checkEsc(screenGraphics); rs.checkForResize(); ks.update(); checkMouseState(); //g.drawImage(screenImage, 0, 0, ul.contentPlaneX, ul.contentPlaneY, null); //screenGraphics.dispose(); return screenImage; } } 

不要在paintComponent方法中更新状态,绘制可能由于任何数量原因而发生,其中许多原因是您未启动或将收到通知。 相反,只应通过“主循环”更新状态

有关绘画如何在Swing中工作的更多详细信息,请参阅在AWT和Swing中绘画

更新

基于Swing Timer的解决方案……

该示例允许您为1-10,000个精灵设置动画,每个精灵独立移动和旋转。 显然,我没有碰撞检测,但动画整体运行良好

很多小马

 import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.IOException; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; import javax.imageio.ImageIO; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JSlider; import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; public class Test { public static void main(String[] args) { new Test(); } public Test() { EventQueue.invokeLater(new Runnable() { @Override public void run() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) { ex.printStackTrace(); } PaintPane pane = new PaintPane(); JSlider slider = new JSlider(1, 10000); slider.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { try { pane.setQuantity(slider.getValue()); } catch (IOException ex) { ex.printStackTrace(); } } }); slider.setValue(1); JFrame frame = new JFrame("Testing"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.add(pane); frame.add(slider, BorderLayout.SOUTH); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); } }); } public static class PaintPane extends JPanel { private static final int SPOOL_DELTA = 100; private List pool; private List sprites; private int quantity; public PaintPane() { try { BufferedImage img = ImageIO.read(getClass().getResource("/resources/Pony.png")); pool = new ArrayList<>(128); sprites = new ArrayList<>(128); Timer timer = new Timer(40, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (sprites.size() < quantity) { List toAdd = new ArrayList<>(SPOOL_DELTA); int required = quantity - sprites.size(); if (pool.isEmpty()) { for (int index = 0; index < Math.min(SPOOL_DELTA, required); index++) { int x = (int)(Math.random() * getWidth()); int y = (int)(Math.random() * getHeight()); toAdd.add(new Sprite(img, new Point(x, y))); } } else { toAdd.addAll(pool.subList(0, Math.min(SPOOL_DELTA, pool.size()))); pool.removeAll(toAdd); } sprites.addAll(toAdd); } else if (sprites.size() > quantity) { List toRemove = new ArrayList<>(SPOOL_DELTA); int required = sprites.size() - quantity; if (sprites.size() > required) { toRemove.addAll(sprites.subList(0, Math.min(SPOOL_DELTA, required))); sprites.removeAll(toRemove); pool.addAll(toRemove); } } for (Sprite sprite : sprites) { sprite.update(getSize()); } repaint(); } }); timer.start(); } catch (IOException ex) { ex.printStackTrace(); } setFont(getFont().deriveFont(Font.BOLD, 18f)); } @Override public Dimension getPreferredSize() { return new Dimension(200, 200); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g.create(); for (Sprite sprite : sprites) { sprite.draw(g2d, this); } String text = NumberFormat.getNumberInstance().format(sprites.size()); FontMetrics fm = g2d.getFontMetrics(); int x = getWidth() - fm.stringWidth(text); int y = (getHeight() - fm.getHeight()) + fm.getAscent(); g2d.drawString(text, x, y); g2d.dispose(); } public void setQuantity(int value) throws IOException { this.quantity = value; } } public static class Sprite { private BufferedImage img; private Point location; private double angle; private Point delta; private double angleDelta; public Sprite(BufferedImage cache, Point location) { img = cache; this.location = new Point(location); delta = new Point(rnd(), rnd()); while (angleDelta == 0) { angleDelta = (Math.random() * 5) - 2.5; } } protected int rnd() { int value = 0; while (value == 0) { value = (int) (Math.random() * 9) - 4; } return value; } public void update(Dimension size) { location.x += delta.x; location.y += delta.y; if (location.x < 0) { location.x = 0; delta.x *= -1; } if (location.x + img.getWidth() > size.width) { location.x = size.width - img.getWidth(); delta.x *= -1; } if (location.y < 0) { location.y = 0; delta.y *= -1; } if (location.y + img.getHeight() > size.height) { location.y = size.height - img.getHeight(); delta.y *= -1; } angle += angleDelta; } public void draw(Graphics2D g2d, JComponent parent) { Graphics2D g = (Graphics2D) g2d.create(); AffineTransform at = AffineTransform.getTranslateInstance(location.x, location.y); at.rotate(Math.toRadians(angle), img.getWidth() / 2, img.getHeight() / 2); g.transform(at); g.drawImage(img, 0, 0, parent); g.dispose(); } } } 

您也可以使用基于“时间”的动画,而不是基于线性的动画

  • 将一个正方形从起点移动到以固定速度点击鼠标的位置
  • JPanel图像从屏幕上飞过
  • Java图像沿列表中的点移动并使用线性插值

如果你真的很勇敢, 将JLabel移动到其他JLabel – GUI并以javaforms以螺旋方式移动图像,这是基于关键帧的动画示例(基于时间)

更新

这是对使用基于时间的动画的问题的原始发布代码的更新,并向对象添加一些旋转(以及一些其他图形更新)。

你会注意到我在更新或绘制形状的关键点周围使用了ReentrantLock ,这可以防止可能的竞争条件或脏读/写发生

以下是10,5,2和1秒持续时间的相同动画

10秒 5秒 2秒 1秒

我注意到的一件事是,更新范围(即窗口)越小,动画效果越好,因此您可以考虑使用repaint(Rectangle)类的东西来减少组件尝试更新的区域数量

 import java.awt.Color; import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; public class Main extends JPanel { double x = 0, y = 0; private Rectangle2D shape; private double angel = 0; private ReentrantLock updateLock = new ReentrantLock(); public JFrame window = new JFrame("Window"); public Main() { window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); window.setSize(1000, 500); window.add(this); window.setVisible(true); } public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g.create(); g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE); g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); g2d.setColor(Color.red); updateLock.lock(); try { g2d.setTransform(AffineTransform.getRotateInstance(Math.toRadians(angel), shape.getCenterX(), shape.getCenterY())); g2d.fill(shape); } finally { updateLock.unlock(); } g2d.dispose(); } public void start() { shape = new Rectangle2D.Double(x, y, 50, 50); Thread t = new Thread(new Runnable() { @Override public void run() { long startTime = System.nanoTime(); long runTime = TimeUnit.NANOSECONDS.convert(10, TimeUnit.SECONDS); System.out.println(runTime); double rotateFrom = 0; double rotateTo = 720; while (true) { long now = System.nanoTime(); long diff = now - startTime; double progress = diff / (double) runTime; if (progress > 1.0d) { progress = 0d; startTime = System.nanoTime(); } x = (getWidth() * progress); updateLock.lock(); try { angel = rotateFrom + ((rotateTo - rotateFrom) * progress); shape.setRect(x, y, 50, 50); } finally { updateLock.unlock(); } repaint(); try { Thread.sleep(8); } catch (InterruptedException e) { e.printStackTrace(); } } } }); t.setDaemon(true); t.start(); } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) { ex.printStackTrace(); } Main game = new Main(); game.start(); } }); } }