介绍
我将您的代码复制到我的 Eclipse IDE 中并按原样运行。我收到以下运行时错误。
Exception in thread "main" java.awt.IllegalComponentStateException: The frame is displayable.
at java.desktop/java.awt.Frame.setUndecorated(Frame.java:926)
at com.ggl.testing.SnakeGame$GameFrame.<init>(SnakeGame.java:41)
at com.ggl.testing.SnakeGame.main(SnakeGame.java:24)
Oracle 有一个有用的教程,使用 Swing 创建 GUI https://docs.oracle.com/javase/tutorial/uiswing/index.html。跳过使用 NetBeans IDE 学习 Swing 部分。密切关注Swing 中的并发 https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html部分。
我编写 Swing 代码已有 10 多年了,并且我在浏览器中添加了 Oracle 网站书签。我仍然会查找如何使用某些组件以确保我正确使用它们。
我做的第一件事就是减慢你的蛇的速度,这样我就可以测试游戏了。我将延迟 75 更改为延迟 750。这是当前 GUI 的屏幕截图。
查看您的代码,您扩展了JFrame
。你不需要延长JFrame
。你没有改变任何JFrame
功能。使用a更简单JFrame
。这引出了我的 Java 规则之一。
不要扩展 Swing 组件或任何 Java 类,除非您想要
重写一个或多个类方法。
你确实延长了JPanel
。没关系,因为你覆盖了paintComponent
method.
最后,你的JPanel
班级做的作业太多了。您还大量使用静态字段。即使您只会创建一个JPanel
,将每个类视为将创建该类的多个实例是一个好习惯。这会减少你日后遇到的问题。
您的想法是正确的,创建了三个类。
因此,让我们使用一些基本模式和 Swing 最佳实践来重新编写您的代码。此时,我不知道我们最终会创建多少个类。
解释
当我编写 Swing GUI 时,我使用模型-视图-控制器 https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller(MVC)模式。顾名思义,您首先创建模型,然后创建视图,最后创建控制器。
应用程序模型由一个或多个普通 Java getter/setter 类组成。
一个视图由一个JFrame
, 一个或多个JPanels
,以及任何其他必要的 Swing 组件。
控制器由一个或多个Actions
or ActionListeners
。在 Swing 中,通常没有一个控制器可以“统治所有”。
总结一下:
- 视图从模型中读取信息
- 视图不更新模型
- 控制器更新模型并重新绘制/重新验证您的视图。
Model
我创建了两个模型类,SnakeModel
and Snake
.
The SnakeModel
class 是一个普通的 Java getter/setter 类,它包含一个Snake
例如,吃掉的苹果数、苹果位置、游戏区域的大小以及一些布尔值。一个布尔值指示游戏循环是否正在运行,另一个布尔值指示游戏是否结束。
游戏区使用了java.awt.Dimension
保存游戏区域的宽度和高度。宽度和高度不必具有相同的值。游戏区域可以是矩形的。
游戏区域以单位来衡量。在视图中,我将单位转换为像素。这与你所做的相反。如果你想改变游戏区域,你所要做的就是改变游戏中的尺寸SnakeModel
班级。视图中的所有内容均基于游戏区域尺寸。
The Snake
班级持有java.util.List
of java.awt.Point
对象和一个char
方向。 Ajava.awt.Point
对象保存 X 和 Y 值。由于我们处理的是对象,而不是 int 值,因此当我们想要一个新的对象时,我们必须小心克隆对象Point
.
View
所有 Swing 应用程序都必须以调用SwingUtilities
invokeLater
方法。此方法确保 Swing 组件在事件调度线程上创建和执行。
我创建了一个JFrame
, 一幅画JPanel
,和一个单独的按钮JPanel
。一般来说,将 Swing 组件添加到绘图中并不是一个好主意JPanel
。通过创建一个单独的按钮JPanel
,我几乎无需额外付费即可获得“开始游戏”按钮的附加功能。游戏运行时该按钮被禁用。
The JFrame
必须按特定顺序调用方法。这setVisible
method 必须最后调用.
我画了图JPanel
通过为乐谱添加单独的区域来使情况变得更加复杂。
我画了图JPanel
只需根据应用程序模型绘制游戏状态即可降低复杂性。时期。没有其他的。
我将随机颜色限制在色谱的白色端,以保持蛇和绘图之间的对比度JPanel
背景。
我使用键绑定而不是键侦听器。优点之一是绘图JPanel
不一定要聚焦。因为我有一个单独的按钮JPanel
, 绘图JPanel
没有焦点。
另一个优点是我可以用四行附加代码添加 WASD 键。
一个缺点是键绑定代码看起来比键侦听器更复杂。一旦您编写了一些按键绑定的代码,您就会体会到其中的优势。
控制器
我创建了三个控制器类,ButtonListener
, TimerListener
, and MovementAction
.
The ButtonListener
类工具ActionListener
. The ButtonListener
类初始化游戏模型并重新启动计时器。
The TimerListener
类工具ActionListener
. The TimerListener
类是游戏循环。该类移动蛇,检查苹果是否被吃掉,检查蛇是否移出游戏区域或接触自身,并重新绘制绘图JPanel
。我使用您的代码作为此类中的代码的模型。
The MovementAction
类扩展AbstractAction
. The AbstractAction
类工具Action
。这个类根据按键改变蛇的方向。
我创建了四个实例MovementAction
类,每个方向一个。这使得actionPerformed
类的方法就简单多了。
Images
这是启动游戏时修改后的 GUI 的样子。
这是游戏期间修改后的 GUI。
这是游戏结束时修改后的 GUI。
Code
这是完整的可运行代码。我创建了所有附加类的内部类,因此我可以将此代码作为一个块发布。
您应该将单独的类放在单独的文件中。
设置 Swing GUI 项目时,我为模型、视图和控制器创建单独的包。这有助于我保持代码的组织性。
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class SnakeGame implements Runnable {
public static void main(String arg[]) {
SwingUtilities.invokeLater(new SnakeGame());
}
private final GamePanel gamePanel;
private final JButton restartButton;
private final SnakeModel model;
public SnakeGame() {
this.model = new SnakeModel();
this.restartButton = new JButton("Start Game");
this.gamePanel = new GamePanel(model);
}
@Override
public void run() {
JFrame frame = new JFrame("Snake");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(gamePanel, BorderLayout.CENTER);
frame.add(createButtonPanel(), BorderLayout.SOUTH);
frame.pack();
frame.setResizable(false);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private JPanel createButtonPanel() {
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
panel.setBackground(Color.black);
restartButton.addActionListener(new ButtonListener(this, model));
panel.add(restartButton);
return panel;
}
public JButton getRestartButton() {
return restartButton;
}
public void repaint() {
gamePanel.repaint();
}
public class GamePanel extends JPanel {
private static final long serialVersionUID = 1L;
private final int margin, scoreAreaHeight, unitSize;
private final Random random;
private final SnakeModel model;
public GamePanel(SnakeModel model) {
this.model = model;
this.margin = 10;
this.unitSize = 25;
this.scoreAreaHeight = 36 + margin;
this.random = new Random();
this.setBackground(Color.black);
Dimension gameArea = model.getGameArea();
int width = gameArea.width * unitSize + 2 * margin;
int height = gameArea.height * unitSize + 2 * margin + scoreAreaHeight;
this.setPreferredSize(new Dimension(width, height));
setKeyBindings();
}
private void setKeyBindings() {
InputMap inputMap = this.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = this.getActionMap();
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "up");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "down");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "left");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "right");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0), "up");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0), "down");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0), "left");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0), "right");
actionMap.put("up", new MovementAction(model, 'U', 'D'));
actionMap.put("down", new MovementAction(model, 'D', 'U'));
actionMap.put("left", new MovementAction(model, 'L', 'R'));
actionMap.put("right", new MovementAction(model, 'R', 'L'));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Dimension gameArea = model.getGameArea();
drawHorizontalGridLines(g, gameArea);
drawVerticalGridLines(g, gameArea);
drawSnake(g);
drawScore(g, gameArea);
if (model.isGameOver) {
drawGameOver(g, gameArea);
} else {
drawApple(g);
}
}
private void drawHorizontalGridLines(Graphics g, Dimension gameArea) {
int y1 = scoreAreaHeight + margin;
int y2 = y1 + gameArea.height * unitSize;
int x = margin;
for (int index = 0; index <= gameArea.width; index++) {
g.drawLine(x, y1, x, y2);
x += unitSize;
}
}
private void drawVerticalGridLines(Graphics g, Dimension gameArea) {
int x1 = margin;
int x2 = x1 + gameArea.width * unitSize;
int y = margin + scoreAreaHeight;
for (int index = 0; index <= gameArea.height; index++) {
g.drawLine(x1, y, x2, y);
y += unitSize;
}
}
private void drawApple(Graphics g) {
// Draw apple
g.setColor(Color.red);
Point point = model.getAppleLocation();
if (point != null) {
int a = point.x * unitSize + margin + 1;
int b = point.y * unitSize + margin + scoreAreaHeight + 1;
g.fillOval(a, b, unitSize - 2, unitSize - 2);
}
}
private void drawScore(Graphics g, Dimension gameArea) {
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 36));
FontMetrics metrics = getFontMetrics(g.getFont());
int width = 2 * margin + gameArea.width * unitSize;
String text = "SCORE: " + model.getApplesEaten();
int textWidth = metrics.stringWidth(text);
g.drawString(text, (width - textWidth) / 2, g.getFont().getSize());
}
private void drawSnake(Graphics g) {
// Draw snake
Snake snake = model.getSnake();
List<Point> cells = snake.getCells();
Point cell = cells.get(0);
drawSnakeCell(g, cell, Color.green);
for (int index = 1; index < cells.size(); index++) {
// Color color = new Color(45, 180, 0);
// random color
Color color = new Color(getColorValue(), getColorValue(),
getColorValue());
cell = cells.get(index);
drawSnakeCell(g, cell, color);
}
}
private void drawSnakeCell(Graphics g, Point point, Color color) {
int x = margin + point.x * unitSize;
int y = margin + scoreAreaHeight + point.y * unitSize;
if (point.y >= 0) {
g.setColor(color);
g.fillRect(x, y, unitSize, unitSize);
}
}
private int getColorValue() {
// White has color values of 255
return random.nextInt(64) + 191;
}
private void drawGameOver(Graphics g, Dimension gameArea) {
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 72));
FontMetrics metrics = getFontMetrics(g.getFont());
String text = "Game Over";
int textWidth = metrics.stringWidth(text);
g.drawString(text, (getWidth() - textWidth) / 2, getHeight() / 2);
}
}
public class ButtonListener implements ActionListener {
private final int delay;
private final SnakeGame view;
private final SnakeModel model;
private final Timer timer;
public ButtonListener(SnakeGame view, SnakeModel model) {
this.view = view;
this.model = model;
this.delay = 750;
this.timer = new Timer(delay, new TimerListener(view, model));
}
@Override
public void actionPerformed(ActionEvent event) {
JButton button = (JButton) event.getSource();
String text = button.getText();
if (text.equals("Start Game")) {
button.setText("Restart Game");
}
button.setEnabled(false);
model.initialize();
timer.restart();
}
}
public class TimerListener implements ActionListener {
private final SnakeGame view;
private final SnakeModel model;
public TimerListener(SnakeGame view, SnakeModel model) {
this.view = view;
this.model = model;
}
@Override
public void actionPerformed(ActionEvent event) {
moveSnake();
checkApple();
model.checkCollisions();
if (model.isGameOver()) {
Timer timer = (Timer) event.getSource();
timer.stop();
model.setRunning(false);
view.getRestartButton().setEnabled(true);
}
view.repaint();
}
private void moveSnake() {
Snake snake = model.getSnake();
Point head = (Point) snake.getHead().clone();
switch (snake.getDirection()) {
case 'U':
head.y--;
break;
case 'D':
head.y++;
break;
case 'L':
head.x--;
break;
case 'R':
head.x++;
break;
}
snake.removeTail();
snake.addHead(head);
// System.out.println(Arrays.toString(cells.toArray()));
}
private void checkApple() {
Point appleLocation = model.getAppleLocation();
Snake snake = model.getSnake();
Point head = snake.getHead();
Point tail = (Point) snake.getTail().clone();
if (head.x == appleLocation.x && head.y == appleLocation.y) {
model.incrementApplesEaten();
snake.addTail(tail);
model.generateRandomAppleLocation();
}
}
}
public class MovementAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private final char newDirection, oppositeDirection;
private final SnakeModel model;
public MovementAction(SnakeModel model, char newDirection,
char oppositeDirection) {
this.model = model;
this.newDirection = newDirection;
this.oppositeDirection = oppositeDirection;
}
@Override
public void actionPerformed(ActionEvent event) {
if (model.isRunning()) {
Snake snake = model.getSnake();
char direction = snake.getDirection();
if (direction != oppositeDirection && direction != newDirection) {
snake.setDirection(newDirection);
// System.out.println("New direction: " + newDirection);
}
}
}
}
public class SnakeModel {
private boolean isGameOver, isRunning;
private int applesEaten;
private Dimension gameArea;
private Point appleLocation;
private Random random;
private Snake snake;
public SnakeModel() {
this.random = new Random();
this.snake = new Snake();
this.gameArea = new Dimension(24, 24);
}
public void initialize() {
this.isRunning = true;
this.isGameOver = false;
this.snake.initialize();
this.applesEaten = 0;
Point point = generateRandomAppleLocation();
// Make sure first apple isn't under snake
int y = (point.y == 0) ? 1 : point.y;
this.appleLocation = new Point(point.x, y);
}
public void checkCollisions() {
Point head = snake.getHead();
// Check for snake going out of the game area
if (head.x < 0 || head.x > gameArea.width) {
isGameOver = true;
return;
}
if (head.y < 0 || head.y > gameArea.height) {
isGameOver = true;
return;
}
// Check for snake touching itself
List<Point> cells = snake.getCells();
for (int index = 1; index < cells.size(); index++) {
Point cell = cells.get(index);
if (head.x == cell.x && head.y == cell.y) {
isGameOver = true;
return;
}
}
}
public Point generateRandomAppleLocation() {
int x = random.nextInt(gameArea.width);
int y = random.nextInt(gameArea.height);
this.appleLocation = new Point(x, y);
return getAppleLocation();
}
public void incrementApplesEaten() {
this.applesEaten++;
}
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
public boolean isGameOver() {
return isGameOver;
}
public void setGameOver(boolean isGameOver) {
this.isGameOver = isGameOver;
}
public Dimension getGameArea() {
return gameArea;
}
public int getApplesEaten() {
return applesEaten;
}
public Point getAppleLocation() {
return appleLocation;
}
public Snake getSnake() {
return snake;
}
}
public class Snake {
private char direction;
private List<Point> cells;
public Snake() {
this.cells = new ArrayList<>();
initialize();
}
public void initialize() {
this.direction = 'R';
cells.clear();
for (int x = 5; x >= 0; x--) {
cells.add(new Point(x, 0));
}
}
public void addHead(Point head) {
cells.add(0, head);
}
public void addTail(Point tail) {
cells.add(tail);
}
public void removeTail() {
cells.remove(cells.size() - 1);
}
public Point getHead() {
return cells.get(0);
}
public Point getTail() {
return cells.get(cells.size() - 1);
}
public char getDirection() {
return direction;
}
public void setDirection(char direction) {
this.direction = direction;
}
public List<Point> getCells() {
return cells;
}
}
}