好吧,为此,我们必须编写一个适当的游戏循环。让我们一个一个开始:
这是我的GameFrame:
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public abstract class GameFrame extends JFrame
{
private GamePanel gamePanel;
public GameFrame (String gameTitle, GamePanel gamePanel)
{
super (gameTitle);
this.gamePanel = gamePanel;
setDefaultCloseOperation (WindowConstants.EXIT_ON_CLOSE);
addWindowListener (new FrameListener ());
getContentPane ().setLayout (new GridBagLayout ());
getContentPane ().add (gamePanel);
pack ();
setLocationRelativeTo (null);
setResizable (false);
setVisible (true);
}
public class FrameListener extends WindowAdapter
{
public void windowActivated (WindowEvent event)
{
gamePanel.setWindowPaused (false);
}
public void windowDeactivated (WindowEvent event)
{
gamePanel.setWindowPaused (true);
}
public void windowDeiconified (WindowEvent event)
{
gamePanel.setWindowPaused (false);
}
public void windowIconified (WindowEvent event)
{
gamePanel.setWindowPaused (true);
}
public void windowClosing (WindowEvent event)
{
gamePanel.stopGame ();
}
}
}
它是一个抽象类,它所做的只是在其中放入一个GamePanel,并在初始化时使其可见。
这是我的GamePanel,它实现了一个游戏循环:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
public abstract class GamePanel extends JPanel implements Runnable
{
private int panelWidth;
private int panelHeight;
private Thread animator;
private volatile boolean running = false;
private volatile boolean isUserPaused = false;
private volatile boolean isWindowPaused = false;
private Graphics2D dbg;
private Image dbImage = null;
private static final int NO_DELAYS_PER_YIELD = 16;
private static final int MAX_FRAME_SKIPS = 5;
private Color backgroundColor;
private long period;
public GamePanel (int width, int height, long fps, Color backgroundColor)
{
this.panelWidth = width;
this.panelHeight = height;
this.backgroundColor = backgroundColor;
this.period = 1000000L * (long) 1000.0 / fps;
setBackground (backgroundColor);
setPreferredSize (new Dimension (panelWidth, panelHeight));
setFocusable (true);
requestFocus ();
readyForPause ();
addKeyListener (new KeyAdapter ()
{
public void keyPressed (KeyEvent e)
{
consumeKeyPressed (e.getKeyCode ());
}
});
}
protected abstract void consumeKeyPressed (int keyCode);
protected abstract void renderGame (Graphics2D graphics);
protected abstract void updateGame ();
@Override
public void addNotify ()
{
super.addNotify ();
startGame ();
}
protected void startGame ()
{
if (animator == null || ! running)
{
animator = new Thread (this);
animator.start ();
}
}
protected void stopGame ()
{
running = false;
}
private void readyForPause ()
{
addKeyListener (new KeyAdapter ()
{
public void keyPressed (KeyEvent e)
{
int keyCode = e.getKeyCode ();
if ((keyCode == KeyEvent.VK_ESCAPE) || (keyCode == KeyEvent.VK_Q)
|| (keyCode == KeyEvent.VK_END) || (keyCode == KeyEvent.VK_P)
|| ((keyCode == KeyEvent.VK_C) && e.isControlDown ()))
{
setUserPaused (! isUserPaused);
}
}
});
}
public void run ()
{
long beforeTime, afterTime, timeDiff, sleepTime;
long overSleepTime = 0L;
int noDelays = 0;
long excess = 0L;
beforeTime = System.nanoTime ();
running = true;
while (running)
{
requestFocus ();
gameUpdate ();
gameRender ();
paintScreen ();
afterTime = System.nanoTime ();
timeDiff = afterTime - beforeTime;
sleepTime = (period - timeDiff) - overSleepTime;
if (sleepTime > 0)
{
try
{
Thread.sleep (sleepTime / 1000000L);
}
catch (InterruptedException ignored)
{
}
overSleepTime = (System.nanoTime () - afterTime - sleepTime);
}
else
{
excess -= sleepTime;
overSleepTime = 0L;
if (++ noDelays >= NO_DELAYS_PER_YIELD)
{
Thread.yield ();
noDelays = 0;
}
}
beforeTime = System.nanoTime ();
int skips = 0;
while ((excess > period) && (skips < MAX_FRAME_SKIPS))
{
excess -= period;
gameUpdate ();
skips++;
}
}
System.exit (0);
}
private void gameUpdate ()
{
if (! isUserPaused && ! isWindowPaused)
{
updateGame ();
}
}
private void gameRender ()
{
if (dbImage == null)
{
dbImage = createImage (panelWidth, panelHeight);
if (dbImage == null)
{
System.out.println ("Image is null.");
return;
}
else
{
dbg = (Graphics2D) dbImage.getGraphics ();
}
}
dbg.setColor (backgroundColor);
dbg.fillRect (0, 0, panelWidth, panelHeight);
renderGame (dbg);
}
private void paintScreen ()
{
Graphics2D g;
try
{
g = (Graphics2D) this.getGraphics ();
if ((g != null) && (dbImage != null))
{
g.drawImage (dbImage, 0, 0, null);
}
Toolkit.getDefaultToolkit ().sync ();
if (g != null)
{
g.dispose ();
}
}
catch (Exception e)
{
System.out.println ("Graphics context error : " + e);
}
}
public void setWindowPaused (boolean isPaused)
{
isWindowPaused = isPaused;
}
public void setUserPaused (boolean isPaused)
{
isUserPaused = isPaused;
}
}
这又是一个抽象类。它的抽象用于可重用目的。你不需要知道我的游戏循环的确切实现。您可以创建自己的自定义游戏面板,继承它,并实现它的抽象方法,一切都会好起来的。
现在让我们创建一个Box:
import java.awt.*;
public class Box extends Rectangle
{
private Color color;
private Direction currentDirection = Direction.None;
private int speed;
public Box (int size, int speed, Color color)
{
super (size, size);
this.speed = speed;
this.color = color;
}
public void update ()
{
switch (currentDirection)
{
case None:
break;
case North:
y -= speed;
break;
case South:
y += speed;
break;
case East:
x += speed;
break;
case West:
x -= speed;
break;
}
}
public void draw (Graphics2D graphics)
{
graphics.setColor (color);
graphics.fill (this);
}
public void setDirection (Direction direction)
{
currentDirection = direction;
}
}
这里没有什么特别之处。它是一个Rectangle 形状,它有一个update 方法来根据它拥有的Direction 更新其状态,还有一个draw 方法使用graphics 对象作为上下文在屏幕上呈现它。
Box 中使用的Direction 枚举如下所示:
public enum Direction
{
None,
North,
South,
East,
West
}
现在是时候创建我们自己的自定义 BoxPanel 了,它将继承自 GamePanel。下面是它的样子:
import java.awt.*;
import java.awt.event.KeyEvent;
public class BoxPanel extends GamePanel
{
private Box box;
public BoxPanel ()
{
super (800, 600, 60, Color.lightGray);
box = new Box (80, 5, Color.darkGray);
}
@Override
protected void consumeKeyPressed (int keyCode)
{
switch (keyCode)
{
case KeyEvent.VK_W:
box.setDirection (Direction.North);
break;
case KeyEvent.VK_S:
box.setDirection (Direction.South);
break;
case KeyEvent.VK_D:
box.setDirection (Direction.East);
break;
case KeyEvent.VK_A:
box.setDirection (Direction.West);
break;
case KeyEvent.VK_SPACE:
box.setDirection (Direction.None);
break;
}
}
@Override
protected void renderGame (Graphics2D graphics)
{
box.draw (graphics);
}
@Override
protected void updateGame ()
{
box.update ();
}
}
它基本上创建了一个box 并实现了GamePanel 的抽象方法,它所做的只是更新盒子并使用现有的适当方法渲染盒子。
consumeKeyPressed 是关于处理按键的,我所做的只是适当地设置box 的方向。
我的BoxFrame 终于来了,它将所有内容打包成一个可运行的演示:
public class BoxFrame extends GameFrame
{
public BoxFrame ()
{
super ("Box Demo", new BoxPanel ());
}
public static void main (String[] args)
{
new BoxFrame ();
}
}
就是这样!
您也可以在自己的项目中使用GameFrame 和GamePanel。抽象确实有回报。不是吗?
如果您不了解部分游戏循环或任何内容,您不必担心。通过反复阅读代码,您最终会理解它。
这是一个可运行的演示,展示了如何创建流畅的动作。我建议您也考虑使用插值来创建平滑的运动。
您可以在创建BoxPanel 的同时调整FPS 的值,以改变游戏的流畅度。
运行代码,阅读它,重新阅读它,理解它。然后自己写作为练习。
仅供参考,我的游戏循环使用一个名为 Double Buffering 的概念来平滑地在屏幕上渲染对象。
您也可以创建具有update 和draw 方法的其他对象,并且可以将它们的调用放在自定义面板的updateGame 和renderGame 方法中,并且这些对象也将被适当地呈现。