So I'm trying to figure out a tutorial on Tic Tac Toe. But I am wondering where i initialize the Computer player.
So I have an abstract AIPlayer Class
public abstract class AIPlayer {
protected int ROWS = 3; // number of rows
protected int COLS = 3; // number of columns
protected Cell[][] cells; // the board's ROWS-by-COLS array of Cells
protected Seed mySeed; // computer's seed
protected Seed oppSeed; // opponent's seed
/** Constructor with reference to game board */
public AIPlayer(Board board) {
cells = board.cells;
}
/** Set/change the seed used by computer and opponent */
public void setSeed(Seed seed) {
this.mySeed = seed;
oppSeed = (mySeed == Seed.CROSS) ? Seed.NOUGHT : Seed.CROSS;
}
/** Abstract method to get next move. Return int[2] of {row, col} */
abstract int[] move(); // to be implemented by subclasses
}
This is my Algorithm to find out the best move for the Computer
import AIPlayer;
import Board;
import Seed;
import java.util.*;
/** AIPlayer using Minimax algorithm */
public class AIPlayerMinimax extends AIPlayer {
/** Constructor with the given game board */
public AIPlayerMinimax(Board board) {
super(board);
}
int[] move() {
int[] result = minimax(2, mySeed); // depth, max turn
return new int[] {result[1], result[2]}; // row, col
}
private int[] minimax(int depth, Seed player) {
//minmax code
}
private List<int[]> generateMoves() {
//generate moves
}
private int evaluate() {
//Evaluating
}
private int evaluateLine(int row1, int col1, int row2, int col2, int row3, int col3) {
// ..
}
private int[] winningPatterns = {
//
};
private boolean hasWon(Seed thePlayer) {
//
}
}
And this is my GameMain which has the graphics and all the good stuff
public class GameMain extends JPanel {
// Named-constants for the game board
public static final int ROWS = 3; // ROWS by COLS cells
public static final int COLS = 3;
public static final String TITLE = "Tic Tac Toe";
// Name-constants for the various dimensions used for graphics drawing
public static final int CELL_SIZE = 100; // cell width and height (square)
public static final int CANVAS_WIDTH = CELL_SIZE * COLS; // the drawing canvas
public static final int CANVAS_HEIGHT = CELL_SIZE * ROWS;
public static final int GRID_WIDTH = 8; // Grid-line's width
public static final int GRID_WIDHT_HALF = GRID_WIDTH / 2; // Grid-line's half-width
// Symbols (cross/nought) are displayed inside a cell, with padding from border
public static final int CELL_PADDING = CELL_SIZE / 6;
public static final int SYMBOL_SIZE = CELL_SIZE - CELL_PADDING * 2;
public static final int SYMBOL_STROKE_WIDTH = 8; // pen's stroke width
private Board board; // the game board
private GameState currentState; // the current state of the game
private Seed currentPlayer; // the current player
private JLabel statusBar; // for displaying status message
/** Constructor to setup the UI and game components */
public GameMain() {
// This JPanel fires MouseEvent
this.addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) { // mouse-clicked handler
int mouseX = e.getX();
int mouseY = e.getY();
// Get the row and column clicked
int rowSelected = mouseY / CELL_SIZE;
int colSelected = mouseX / CELL_SIZE;
if (currentState == GameState.PLAYING) {
if (rowSelected >= 0 && rowSelected < ROWS
&& colSelected >= 0 && colSelected < COLS
&& board.cells[rowSelected][colSelected].content == Seed.EMPTY) {
board.cells[rowSelected][colSelected].content = currentPlayer; // move
updateGame(currentPlayer, rowSelected, colSelected); // update currentState
// Switch player
currentPlayer = (currentPlayer == Seed.CROSS) ? Seed.NOUGHT : Seed.CROSS;
}
} else { // game over
initGame(); // restart the game
}
// Refresh the drawing canvas
repaint(); // Call-back paintComponent().
}
});
// Setup the status bar (JLabel) to display status message
statusBar = new JLabel(" ");
statusBar.setFont(new Font(Font.DIALOG_INPUT, Font.BOLD, 14));
statusBar.setBorder(BorderFactory.createEmptyBorder(2, 5, 4, 5));
statusBar.setOpaque(true);
statusBar.setBackground(Color.LIGHT_GRAY);
setLayout(new BorderLayout());
add(statusBar, BorderLayout.PAGE_END); // same as SOUTH
setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT + 30));
// account for statusBar in height
board = new Board(); // allocate the game-board
initGame(); // Initialize the game variables
}
/** Initialize the game-board contents and the current-state */
public void initGame() {
for (int row = 0; row < ROWS; ++row) {
for (int col = 0; col < COLS; ++col) {
board.cells[row][col].content = Seed.EMPTY; // all cells empty
}
}
currentState = GameState.PLAYING; // ready to play
currentPlayer = Seed.CROSS; // cross plays first
}
/** Update the currentState after the player with "theSeed" has placed on (row, col) */
public void updateGame(Seed theSeed, int row, int col) {
if (board.hasWon(theSeed, row, col)) { // check for win
currentState = (theSeed == Seed.CROSS) ? GameState.CROSS_WON : GameState.NOUGHT_WON;
} else if (board.isDraw()) { // check for draw
currentState = GameState.DRAW;
}
// Otherwise, no change to current state (PLAYING).
}
/** Custom painting codes on this JPanel */
#Override
public void paintComponent(Graphics g) { // invoke via repaint()
super.paintComponent(g); // fill background
setBackground(Color.WHITE); // set its background color
board.paint(g); // ask the game board to paint itself
// Print status-bar message
if (currentState == GameState.PLAYING) {
statusBar.setForeground(Color.BLACK);
if (currentPlayer == Seed.CROSS) {
statusBar.setText("X's Turn");
} else {
statusBar.setText("O's Turn");
}
} else if (currentState == GameState.DRAW) {
statusBar.setForeground(Color.RED);
statusBar.setText("It's a Draw! Click to play again.");
} else if (currentState == GameState.CROSS_WON) {
statusBar.setForeground(Color.RED);
statusBar.setText("'X' Won! Click to play again.");
} else if (currentState == GameState.NOUGHT_WON) {
statusBar.setForeground(Color.RED);
statusBar.setText("'O' Won! Click to play again.");
}
}
/** The entry "main" method */
public static void main(String[] args) {
// Run GUI construction codes in Event-Dispatching thread for thread safety
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFrame frame = new JFrame(TITLE);
// Set the content-pane of the JFrame to an instance of main JPanel
frame.setContentPane(new GameMain());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null); // center the application window
frame.setVisible(true); // show it
}
});
}
}
So my question is, how do I initialize the AIPlayer to compete with me? At the moment this works as a multiplayer game, Human vs Human.
Well, you probably want to add a property go GameMain:
private AIPlayer aiPlayer;
Instantiate your AIPlayer in initGame():
aiPlayer = new AIPlayer(board)
aiPlayer.setSeed(Seed.NOUGHT);
Then it depends on when you want the AI Player to make its move. You'd need to call something like this:
public void AIMove() {
int[] generatedMove = aiPlayer.move();
board.cells[generatedMove[0]][generatedMove[1]].content = currentPlayer;
updateGame(currentPlayer, generatedMove[0], generatexMove[1]);
repaint();
}
That should work, but isn't the best code-design. As you can see there is a lot of duplication, no seperation of concerns and other design flaws. But that would be a better question for CodeReview.SE.
Related
My restart button of the game does not work and it multiples when it is clicked. I do not understand Java perfectly I am considering myself good.
Main of the game
package snake_game;
public class snake {
public static void main(String arg[]) {
new GameFrame();
// is exacly the same as frame f = new frame();
// this is shorter and does the same job
}
}
GamePanel
package snake_game;
// import java.awt.event.ActionEvent;
// import java.awt.event.ActionListener;
// import java.awt.Graphics;
// import java.awt.event.KeyAdapter;
// import java.awt.event.KeyEvent;
// or I could write simply
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.Random;
import javax.swing.JPanel;
public class GamePanel extends JPanel implements ActionListener {
// panal dimentions
static final int SCREEN_WIDTH = 600;
static final int SCREEN_HEIGHT = 600;
// panal dimentions
// size
static final int UNIT_SIZE = 25;
// size to make 600 * 600 = 1200 px equel between 25 px
static final int GAME_UNITS = (SCREEN_WIDTH * SCREEN_HEIGHT) / UNIT_SIZE;
// size
// delay how fast the game will be
static final int DELAY = 75;
// delay
// dimentions
final int x[] = new int[GAME_UNITS];
final int y[] = new int[GAME_UNITS];
// dimentions
// snake
int bodyParts = 6;
// snake
// apple
int appleEaten;
int appleX;
int appleY;
// apple
char direction = 'R';
boolean running = false;
Timer timer;
Random random;
GamePanel game;
JButton resetButton;
GamePanel() {
random = new Random();
this.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
this.setBackground(Color.black);
this.setFocusable(true);
this.addKeyListener(new MyKeyAdapter());
startGame();
// when we want to make the program to continie we must say what the programm
// must execute next
}
public void startGame() {
newApple();
running = true;
timer = new Timer(DELAY, this);
timer.start();
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
draw(g);
}
public void draw(Graphics g) {
if (running) {
for (int i = 0; i < SCREEN_HEIGHT / UNIT_SIZE; i++) {
g.drawLine(i * UNIT_SIZE, 0, i * UNIT_SIZE, SCREEN_HEIGHT);
g.drawLine(0, i * UNIT_SIZE, i * SCREEN_WIDTH, i * UNIT_SIZE);
}
g.setColor(Color.red);
g.fillOval(appleX, appleY, UNIT_SIZE, UNIT_SIZE);
for (int i = 0; i < bodyParts; i++) {
if (i == 0) {
g.setColor(Color.green);
g.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE);
} else {
g.setColor(new Color(45, 180, 0));
// random color
g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
// random color
g.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE);
}
}
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 30));
FontMetrics metrics = getFontMetrics(g.getFont());
g.drawString("SCORE:" + appleEaten, (SCREEN_WIDTH - metrics.stringWidth("SCORE:" + appleEaten)) / 2,
g.getFont().getSize());
} else {
gameOver(g);
}
}
public void newApple() {
appleX = random.nextInt((int) (SCREEN_WIDTH / UNIT_SIZE)) * UNIT_SIZE;
appleY = random.nextInt((int) (SCREEN_HEIGHT / UNIT_SIZE)) * UNIT_SIZE;
}
public void move() {
for (int i = bodyParts; i > 0; i--) {
x[i] = x[i - 1];
y[i] = y[i - 1];
}
switch (direction) {
case 'U':
y[0] = y[0] - UNIT_SIZE;
break;
case 'D':
y[0] = y[0] + UNIT_SIZE;
break;
case 'L':
x[0] = x[0] - UNIT_SIZE;
break;
case 'R':
x[0] = x[0] + UNIT_SIZE;
break;
}
}
public void checkApple() {
if ((x[0] == appleX) && (y[0] == appleY)) {
bodyParts++;
appleEaten++;
newApple();
}
}
public void checkCollisions() {
// check if head collides with body
for (int i = bodyParts; i > 0; i--) {
if ((x[0] == x[i]) && (y[0] == y[i])) {
running = false;
}
}
// check if head touches left border
if (x[0] < 0) {
running = false;
}
// check if head touches right border
if (x[0] > SCREEN_WIDTH) {
running = false;
}
// check if head touches top border
if (y[0] < 0) {
running = false;
}
// check if head touches bottom border
if (y[0] > SCREEN_HEIGHT) {
running = false;
}
if (!running) {
timer.stop();
}
}
public void gameOver(Graphics g) {
// score
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 30));
FontMetrics metrics1 = getFontMetrics(g.getFont());
g.drawString("SCORE:" + appleEaten, (SCREEN_WIDTH - metrics1.stringWidth("SCORE:" + appleEaten)) / 2,
g.getFont().getSize());
// game over text
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 75));
FontMetrics metrics2 = getFontMetrics(g.getFont());
g.drawString("Game Over", (SCREEN_WIDTH - metrics2.stringWidth("Game Over")) / 2, SCREEN_HEIGHT / 2);
// restart button
resetButton = new JButton();
resetButton.setText("Restart");
resetButton.setSize(100, 50);
resetButton.setLocation(150, 150);
resetButton.addActionListener(this);
game = new GamePanel();
this.add(resetButton);
this.add(game);
this.setVisible(true);
// restart button
}
#Override
public void actionPerformed(ActionEvent e) {
if (running) {
move();
checkApple();
checkCollisions();
}
repaint();
// restart button
if (e.getSource() == resetButton) {
this.remove(game);
game = new GamePanel();
this.add(game);
resetButton.setVisible(false);
SwingUtilities.updateComponentTreeUI(this);
// restart button
}
}
public class MyKeyAdapter extends KeyAdapter {
#Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
if (direction != 'R') {
direction = 'L';
}
break;
case KeyEvent.VK_RIGHT:
if (direction != 'L') {
direction = 'R';
}
break;
case KeyEvent.VK_UP:
if (direction != 'D') {
direction = 'U';
}
break;
case KeyEvent.VK_DOWN:
if (direction != 'U') {
direction = 'D';
}
break;
}
}
}
}
GameFrame
package snake_game;
import javax.swing.JFrame;
public class GameFrame extends JFrame {
GameFrame() {
GamePanel panel = new GamePanel();
this.add(panel);
this.setTitle("Snake");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setResizable(false);
this.pack();
this.setVisible(true);
this.setLocationRelativeTo(null);
this.setUndecorated(false);
}
}
Introduction
I copied your code into my Eclipse IDE and ran it as is. I received the following runtime error.
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 has a helpful tutorial, Creating a GUI With Swing. Skip the Learning Swing with the NetBeans IDE section. Pay close attention to the Concurrency in Swing section.
I've been writing Swing code for over 10 years, and I have the Oracle website bookmarked in my browser. I still look up how to use certain components to make sure I'm using them correctly.
The first thing I did was to slow down your snake so I could test the game. I changed the delay of 75 to a delay of 750. Here's a screenshot of your current GUI.
Looking over your code, you extend a JFrame. You don't need to extend a JFrame. You're not changing any JFrame functionality. It's much simpler to use a JFrame. This leads to one of my Java rules.
Don't extend a Swing component, or any Java class, unless you intend
to override one or more of the class methods.
You do extend a JPanel. That's fine because you override the paintComponent method.
Finally, your JPanel class is doing too much work. You also make heavy use of static fields. Even though you'll only create one JPanel, it's a good habit to treat every class as if you will create multiple instances of the class. This creates fewer problems for yourself down the road.
You have the right idea, creating three classes.
So let's rework your code, using some basic patterns and Swing best practices. At this time, I don't know how many classes we'll wind up creating.
Explanation
When I write a Swing GUI, I use the model–view–controller (MVC) pattern. The name implies that you create a model first, then the view, then the controller(s).
An application model consists of one or more plain Java getter/setter classes.
A view consists of a JFrame, one or more JPanels, and any other necessary Swing components.
The controller consists of one or more Actions or ActionListeners. In Swing, there's usually not one controller to "rule them all".
To summarize:
The view reads information from the model
The view does not update the model
The controllers update the model and repaint/revalidate your view.
Model
I created two model classes, SnakeModel and Snake.
The SnakeModel class is a plain Java getter/setter class that holds one Snake instance, the apple eaten count, the apple location, the size of the game area, and a couple of booleans. One boolean indicates whether or not the game loop is running and the other boolean indicates whether or not the game is over.
The game area uses a java.awt.Dimension to hold the width and height of the game area. The width and the height do not have to have the same value. The game area can be rectangular.
The game area is measured in units. In the view, I convert the units into pixels. That's the opposite of what you did. If you want to change the game area, all you have to do is change the dimensions in the SnakeModel class. Everything in the view is based on the game area dimension.
The Snake class holds a java.util.List of java.awt.Point objects and a char direction. A java.awt.Point object holds an X and Y value. Since we're dealing with objects, rather than int values, we have to be careful to clone the object when we want a new Point.
View
All Swing applications must start with a call to the SwingUtilities invokeLater method. This method ensures that the Swing components are created and executed on the Event Dispatch Thread.
I created a JFrame, a drawing JPanel, and a separate button JPanel. Generally, it's not a good idea to add Swing components to a drawing JPanel. By creating a separate button JPanel, I get the added feature of a "Start Game" button at almost no extra cost. The button is disabled while the game is running.
The JFrame methods must be called in a specific order. The setVisible method must be called last.
I made the drawing JPanel more complicated by adding a separate area for the score.
I made the drawing JPanel less complicated by only drawing the state of the game, based on the application model. Period. Nothing else.
I limited the random colors to the white end of the color spectrum to maintain the contrast between the snake and the drawing JPanel background.
I used key bindings instead of a key listener. One advantage is that the drawing JPanel doesn't have to be in focus. Since I have a separate button JPanel, the drawing JPanel doesn't have focus.
Another advantage is that I can add the WASD keys with four lines of additional code.
One disadvantage is that the key bindings code looks more complicated than a key listener. Once you've coded a few key bindings, you'll appreciate the advantages.
Controller
I created three controller classes, ButtonListener, TimerListener, and MovementAction.
The ButtonListener class implements ActionListener. The ButtonListener class initializes the game model and restarts the timer.
The TimerListener class implements ActionListener. The TimerListener class is the game loop. This class moves the snake, checks to see if the apple is eaten, checks to see if the snake has moved outside the game area or touched itself, and repaints the drawing JPanel. I used your code as a model for the code in this class.
The MovementAction class extends AbstractAction. The AbstractAction class implements Action. This class changes the direction of the snake, based on the key presses.
I create four instances of the MovementAction class, one for each direction. This makes the actionPerformed method of the class much simpler.
Images
Here's what the revised GUI looks like when you start the game.
Here's the revised GUI during the game.
Here's the revised GUI when the game is over.
Code
Here's the complete runnable code. I made all the additional classes inner classes so I could post this code as one block.
You should put the separate classes in separate files.
When setting up a Swing GUI project, I create separate packages for the model, view, and controller. This helps me keep the code organized.
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;
}
}
}
JButtons only show up when I hover over them. Also disappear when I resize window. The error lies in the constructor here. I am extending a JFrame. Removing the JPanel doesn't fix it. Please help.
public GameBoard(String title, int width, int height)
{
super();
this.boardWidth = width;
this.boardHeight = height;
// Create game state
this.board = new GameSquare[width][height];
// Set up window
setTitle(title);
setSize(720,720);
setContentPane(boardPanel);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
boardPanel.setLayout(new GridLayout(height,width));
int decorator = 0;
for (int y = 0; y<height; y++){
decorator++;
for (int x = 0; x<width; x++){
if (decorator % 2 == 0)
board[x][y] = new GameSquare(x, y, "BLACK");
else
board[x][y] = new GameSquare(x, y, "WHITE");
board[x][y].addActionListener(this);
boardPanel.add(board[x][y]);
decorator++;
}
}
// make our window visible
setVisible(true);
}
MCV Example.
package chess;
public class GameBoard extends JFrame implements ActionListener
{
private JPanel boardPanel = new JPanel();
private int boardHeight;
private int boardWidth;
private GameSquare[][] board;
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
GameBoard gameBoard = new GameBoard("The Revolutionary War",8,8);
}
});
}
/**
* Create a new game board of the given size.
* As soon as an instance of this class is created, it is visualised
* on the screen.
*
* #param title the name printed in the window bar.
* #param width the width of the game area, in squares.
* #param height the height of the game area, in squares.
*/
public GameBoard(String title, int width, int height)
{
super();
this.boardWidth = width;
this.boardHeight = height;
// Create game state
this.board = new GameSquare[width][height];
// Set up window
setTitle(title);
setSize(720,720);
setContentPane(boardPanel);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
boardPanel.setLayout(new GridLayout(height,width));
for (int y = 0; y<height; y++){
for (int x = 0; x<width; x++){
board[x][y] = new GameSquare(x, y, this);
board[x][y].setEnabled(true);
board[x][y].addActionListener(this);
boardPanel.add(board[x][y]);
}
}
// make our window visible
setVisible(true);
}
/**
* Returns the GameSquare instance at a given location.
* #param x the x co-ordinate of the square requested.
* #param y the y co-ordinate of the square requested.
* #return the GameSquare instance at a given location
* if the x and y co-ordinates are valid - null otherwise.
*/
public GameSquare getSquareAt(int x, int y)
{
if (x < 0 || x >= boardWidth || y < 0 || y >= boardHeight)
return null;
return (GameSquare) board[x][y];
}
public void actionPerformed(ActionEvent e)
{
// The button that has been pressed.s
GameSquare square = (GameSquare)e.getSource();
}
//BufferedImage background = ImageIO.read(new File("/path/to/image.jpg"));
}
The other class is here:
package chess;
public class GameSquare extends JButton {
/**
*
*/
private static final long serialVersionUID = 1L;
private int x;
private int y;
private ChessPiece chessPiece;
private boolean selected;
private ImageIcon icon;
GameSquare(int xPos, int yPos, GameBoard board){
super();
x = xPos;
y = yPos;
chessPiece = null;
selected = false;
// Test to see colour of the base tile
if ((xPos+yPos) % 2 == 1){
icon = new ImageIcon(getClass().getResource("/pics/black.png"));
this.setIcon(icon);
}
else if ((xPos+yPos) % 2 == 0) {
icon = new ImageIcon(getClass().getResource("/pics/white.png"));
this.setIcon(icon);
}
}
public int getX(){
return x;
}
public int getY(){
return y;
}
public void setChessPiece(ChessPiece piece){
chessPiece = piece;
}
public ChessPiece getChessPiece(){
return chessPiece;
}
public void setImage(){
setIcon(chessPiece.getIcon());
}
public void select(){
selected = true;
setIcon(new ImageIcon(getClass().getResource("pics/selected.png")));
}
public void deselect(){
selected = false;
if (chessPiece == null) setIcon(icon);
else setImage();
}
}
Your problem is due to your overriding JPanel's getX() and getY(). These methods are used by the container's layout managers to help place components, and by overriding these key methods, you mess up the ability of the layouts to do this. Please change the names of those methods to something that does not override a key method of the class. If this is a chess square, I'd rename the variables to rank and file, and have getRank() and getFile() methods.
public class GameSquare extends JButton {
private int file;
private int rank;
// ....
GameSquare(int xPos, int yPos, GameBoard board){
super();
file = xPos;
rank = yPos;
chessPiece = null;
selected = false;
// .....
}
public int getFile(){
return file;
}
public int getRank(){
return rank;
}
// .....
}
add
pack();
in the GameBoard class, before setting visible. This will ensure that the size is correct and make room for any buttons which are hanging off the edge... This will make the frame the size of the JPanel, exactly as it is rendered after all objects are added.
also maybe
setResizable(false);
as it will stop you resizing your windows, and with images this can cause buttons to disappear as well as reappear if the user accidentally clicks the sides. I'd put this underneath: setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
I am working on homework for class, and its late because I can't seem to understand the material despite all the research that I am doing. I am a beginner and do not know much in the way of java. Also, this is my first post so please be forgiving when you are reading this.
I am building on source code from my textbook, which I updated recently for past homework, but now I am trying to generate a class that draws multiple squares and moves those objects independently and at different speeds. They will all need to rebound off the walls as well. I followed the instructions and created two arrays that will hold the random x and y values between 1 and 10. However, I struggle with arrays and I am sure that I am not doing it correctly. So, I would love some feedback to see if I have it set up correctly.
I have a the jpanel pulling up and drawing, and as long as there is 1 square it is working fine bouncing off the walls, but things change when I draw more than one. The do not move independently and they also share the same speed. Some even disappear from time to time. This has really thrown me off. I appreciate any help!
In short, I am trying to paint new squares that all travel in different directions and at different speeds. Per the instructions we are suppose create and use a two arrays that handle the x and y values.
Here is what I have so far:
public class DotsPanel extends JPanel
{
private int delay = 15;
private final int SIZE = 7, IMAGE_SIZE = 3; // radius of each dot
private Timer timer;
private int x, y, i;
private ArrayList<Point> pointList;
static int [] xarray = new int [1000];
static int [] yarray = new int [1000];
Random rand = new Random();
//-----------------------------------------------------------------
// Constructor: Sets up this panel to listen for mouse events.
//-----------------------------------------------------------------
public DotsPanel()
{
pointList = new ArrayList<Point>();
int [] xarray = new int [1000];
int [] yarray = new int [1000];
timer = new Timer(delay, new ReboundListener());
addMouseListener (new DotsListener());
addMouseMotionListener (new DotsListener());
setBackground(Color.gray);
setPreferredSize(new Dimension(700, 500));
for(int i = 0; i < xarray.length; i++)
{
xarray[i] = rand.nextInt(7);
yarray[i] = rand.nextInt(7);
}
timer.start();
}
//-----------------------------------------------------------------
// Draws all of the dots stored in the list.
//-----------------------------------------------------------------
public void paintComponent(Graphics page)
{
super.paintComponent(page);
page.setColor(Color.BLUE);
for (Point spot : pointList)
{
page.fillRect(spot.x-SIZE, spot.y-SIZE, 25, 25);
page.drawString("Count: " + pointList.size(), 5, 15);
}
}
//*****************************************************************
// Represents the listener for mouse events.
//*****************************************************************
private class DotsListener implements MouseListener, MouseMotionListener
{
//--------------------------------------------------------------
// Adds the current point to the list of points and redraws
// the panel whenever the mouse button is pressed.
//--------------------------------------------------------------
public void mousePressed(MouseEvent event)
{
pointList.add(event.getPoint());
repaint();
}
public void mouseDragged(MouseEvent event)
{
// initially I had two xarray and yarray in here just like in
// mouseClicked
// but it did not change anything when removed
}
//--------------------------------------------------------------
// Provide empty definitions for unused event methods.
//--------------------------------------------------------------
public void mouseClicked(MouseEvent event)
{
xarray[i] = rand.nextInt(7);
yarray[i] = rand.nextInt(7);
}
public void mouseReleased(MouseEvent event) {}
public void mouseEntered(MouseEvent event) {}
public void mouseExited(MouseEvent event) {}
public void mouseMoved(MouseEvent e) {}
}
private class ReboundListener implements ActionListener
{
//--------------------------------------------------------------
// Updates the position of the image and possibly the direction
// of movement whenever the timer fires an action event.
//--------------------------------------------------------------
public void actionPerformed(ActionEvent event)
{
for (Point spot : pointList)
{
spot.x += xarray[i];
spot.y += yarray[i];
if (spot.x <= 0 || spot.x >= 700)
xarray[i] = xarray[i] * -1;
if (spot.y <= 0 || spot.y >= 500)
yarray[i] = yarray[i] * -1;
repaint();
}
}
}
}
However, I struggle with arrays and I am sure that I am not doing it correctly.
I wouldn't use Arrays.
Instead, have a Ball object manage its own state. Then you can have different color, speed, size etc for each Ball. Then when the Timer fires you just calculate the new position and repaint the Ball.
Here is an example to get you started:
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
public class BallAnimation4
{
private static void createAndShowUI()
{
BallPanel panel = new BallPanel();
JFrame frame = new JFrame("BallAnimation4");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add( panel );
frame.setSize(800, 600);
frame.setLocationRelativeTo( null );
//frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
frame.setVisible( true );
panel.addBalls(5);
panel.startAnimation();
}
public static void main(String[] args)
{
EventQueue.invokeLater(new Runnable()
{
public void run()
{
createAndShowUI();
}
});
}
}
class BallPanel extends JPanel implements ActionListener
{
private ArrayList<Ball> balls = new ArrayList<Ball>();
public BallPanel()
{
setLayout( null );
setBackground( Color.BLACK );
}
public void addBalls(int ballCount)
{
Random random = new Random();
for (int i = 0; i < ballCount; i++)
{
Ball ball = new Ball();
ball.setRandomColor(true);
ball.setLocation(random.nextInt(getWidth()), random.nextInt(getHeight()));
ball.setMoveRate(32, 32, 1, 1, true);
// ball.setMoveRate(16, 16, 1, 1, true);
ball.setSize(32, 32);
balls.add( ball );
}
}
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
for (Ball ball: balls)
{
ball.draw(g);
}
}
public void startAnimation()
{
Timer timer = new Timer(75, this);
timer.start();
}
public void actionPerformed(ActionEvent e)
{
move();
repaint();
}
private void move()
{
for (Ball ball : balls)
{
ball.move(this);
}
}
class Ball
{
public Color color = Color.BLACK;
public int x = 0;
public int y = 0;
public int width = 1;
public int height = 1;
private int moveX = 1;
private int moveY = 1;
private int directionX = 1;
private int directionY = 1;
private int xScale = moveX;
private int yScale = moveY;
private boolean randomMove = false;
private boolean randomColor = false;
private Random myRand = null;
public Ball()
{
myRand = new Random();
setRandomColor(randomColor);
}
public void move(JPanel parent)
{
int iRight = parent.getSize().width;
int iBottom = parent.getSize().height;
x += 5 + (xScale * directionX);
y += 5 + (yScale * directionY);
if (x <= 0)
{
x = 0;
directionX *= (-1);
xScale = randomMove ? myRand.nextInt(moveX) : moveX;
if (randomColor) setRandomColor(randomColor);
}
if (x >= iRight - width)
{
x = iRight - width;
directionX *= (-1);
xScale = randomMove ? myRand.nextInt(moveX) : moveX;
if (randomColor) setRandomColor(randomColor);
}
if (y <= 0)
{
y = 0;
directionY *= (-1);
yScale = randomMove ? myRand.nextInt(moveY) : moveY;
if (randomColor) setRandomColor(randomColor);
}
if (y >= iBottom - height)
{
y = iBottom - height;
directionY *= (-1);
yScale = randomMove ? myRand.nextInt(moveY) : moveY;
if (randomColor) setRandomColor(randomColor);
}
}
public void draw(Graphics g)
{
g.setColor(color);
g.fillOval(x, y, width, height);
}
public void setColor(Color c)
{
color = c;
}
public void setLocation(int x, int y)
{
this.x = x;
this.y = y;
}
public void setMoveRate(int xMove, int yMove, int xDir, int yDir, boolean randMove)
{
this.moveX = xMove;
this.moveY = yMove;
directionX = xDir;
directionY = yDir;
randomMove = randMove;
}
public void setRandomColor(boolean randomColor)
{
this.randomColor = randomColor;
switch (myRand.nextInt(3))
{
case 0: color = Color.BLUE;
break;
case 1: color = Color.GREEN;
break;
case 2: color = Color.RED;
break;
default: color = Color.BLACK;
break;
}
}
public void setSize(int width, int height)
{
this.width = width;
this.height = height;
}
}
}
Since your Arrays only contain the Point you want to paint you don't have any information about the speed each point should be moved at. The best you could do is create a random amount each point should be moved each time its location is changed. This would give erratic movement as each time you move a point the distance would be random.
If you want more constant speed then you would need to create a second Array to contain the distance each point should move every time.
This starts to get messy creating a new Array every time you want a new property to be unique for the object you want to paint. That is why the approach to create a custom Object with multiple properties is easier to manage.
I am new to graphics in java and I am currently working on a game. Essentially, there are rising bubbles, and the user has to pop them by moving the mouse over them.
I have already made the bubbles but now for the collision detection, I need to be able to find the x and coordinates as well as the diameter of the circle. How can I return the instance variables for each bubble (from the Bubbles object array). You can see my code below. If you find coding errors, please let me know.
Game Class:
public class Game extends JPanel{
public static final int WINDOW_WIDTH = 600;
public static final int WINDOW_HEIGHT = 400;
private boolean insideCircle = false;
private static boolean ifPaused = false;
private static JPanel mainPanel;
private static JLabel statusbar;
private static int clicks = 0;
//Creates a Bubbles object Array
Bubbles[] BubblesArray = new Bubbles[5];
public Game() {
//initializes bubble objects
for (int i = 0; i < BubblesArray.length; i++)
BubblesArray[i] = new Bubbles();
}
public void paint(Graphics graphics) {
//makes background white
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
//paints square objects to the screen
for (Bubbles aBubblesArray : BubblesArray) {
aBubblesArray.paint(graphics);
}
}
public void update() {
//calls the Square class update method on the square objects
for (Bubbles aBubblesArray : BubblesArray) aBubblesArray.update();
}
public static void main(String[] args) throws InterruptedException {
statusbar = new JLabel("Default");
Game game = new Game();
game.addMouseMotionListener(new MouseAdapter() {
#Override
public void mouseMoved(MouseEvent e) {
statusbar.setText(String.format("Your mouse is at %d, %d", e.getX(), e.getY()));
}
public void mouseExited(MouseEvent e){
ifPaused = true;
}
public void mouseEntered(MouseEvent e){
ifPaused = false;
}
});
JFrame frame = new JFrame();
frame.add(game);
frame.add(statusbar, BorderLayout.SOUTH);
frame.setVisible(true);
frame.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setTitle("Bubble Burst");
frame.setResizable(false);
frame.setLocationRelativeTo(null);
while (true) {
if (!ifPaused){
game.update();
game.repaint();
Thread.sleep(5);
}
}
}
}
Bubbles Class:
public class Bubbles{
private int circleXLocation;
private int circleSize;
private int circleYLocation = Game.WINDOW_WIDTH + circleSize;
private int fallSpeed = 1;
private int color = (int) (4 * Math.random() + 1);
Random rand = new Random();
private int whereMouseX;
private int whereMouseY;
public int generateRandomXLocation(){
return circleXLocation = (int) (Game.WINDOW_WIDTH * Math.random() - circleSize);
}
public int generateRandomCircleSize(){
return circleSize = (int) (50 * Math.random() + 30);
}
public int generateRandomFallSpeed(){
return fallSpeed = (int) (5 * Math.random() + 1);
}
public void paint(Graphics g){
if (color == 1) g.setColor(new Color(0x87CEEB));
if (color == 2) g.setColor(new Color(0x87CEFF));
if (color == 3) g.setColor(new Color(0x7EC0EE));
if (color == 4) g.setColor(new Color(0x6CA6CD));
g.fillOval (circleXLocation, circleYLocation, circleSize, circleSize);
}
public Bubbles(){
generateRandomXLocation();
generateRandomCircleSize();
generateRandomFallSpeed();
}
public void update(){
if (circleYLocation <= - circleSize){
generateRandomXLocation();
generateRandomFallSpeed();
generateRandomCircleSize();
circleYLocation = Game.WINDOW_HEIGHT + circleSize;
}
if (circleYLocation > - circleSize){
circleYLocation -= fallSpeed;
}
}
public int getX(){
return circleXLocation;
}
public int getY(){
return circleYLocation;
}
public int getCircleSize(){
return circleSize;
}
}
Set your bubbles to include x and y values in it's constructor.
Public Bubble(float x, float y, int circleSize){
// initialize variables here
}
Then check to see if they are colliding with your current mouse location, something like...
if(e.getX() == this.getX() && e.getY() == this.getY()){
// do something
}
Loop through each Bubble in the array and access the public get methods you have created in the Bubble class:
Example:
for (Bubble b : BubblesArray)
{
int x = b.getX();
int y = b.getY();
}
I have been struggling with this problem now for over a week, and would really appreciate some help. I am developing my first Java game using a gui, and I currently have about 20 classes involved. The game is a simple grid-based representation of Star Trek, with JLabel icons that move around the galaxy grid. The problem is that usually after about 7 to 10 moves, one of two things will happen: one, the grid of sectors in my current quadrant will disappear, leaving only a single sector in the top left corner; or two, the Enterprise icon will disappear.
I have no experience dealing with threads, but after some reading I thought this was probably a result of the Event Dispatch Thread not being properly synchronized with the program logic. I read up on the proper way to update a GUI, and surrounded all my statements that had any effect on the GUI (I think) with invokeLater and invokeAndWait blocks.
However, this did not solve the problem. So, today I rewrote everything into the smallest compilable unit I could (it isn't that small, but I can't figure out how to make it smaller) while still keeping my basic game structure to see if that would change anything. It didn't. The GUI still gets corrupted after 7 to 10 moves.
I am at my wits' end here. I would be truly grateful for some help.
Here is my code. It compiles and runs as is.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.Random;
import java.util.Scanner;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SpringLayout;
import javax.swing.SwingUtilities;
import javax.swing.border.BevelBorder;
public class GUI extends JFrame
{
int screenwidth;
int screenheight;
public static void main(String[] args)
{
GUI gui = new GUI();
run(gui);
}
public static void run(final GUI gui)
{
Quadrant[][] galaxy = new Quadrant[8][8];
//populate galaxy with quadrants
for(int i = 0; i < 8; i++)
{
for(int j = 0; j < 8; j++)
{
galaxy[i][j] = new Quadrant(i, j);
}
}
//Quadrant to put in the view when game starts
Quadrant startingQuadrant = galaxy[0][0];
final QuadrantView quadrantView = startingQuadrant.getQuadrantView();
Enterprise enterprise;
Sector startingSector;
//add SectorViews to the QuadrantView
for (int i = 0; i < 8; i++)
{
for(int j = 0; j < 8; j++)
{
startingQuadrant.getQuadrantView().addSectorView(startingQuadrant.getSectorArray()[i][j].getSectorView(), i, j);
}
}
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
//initialize gui with the starting quadrant quadrantView
gui.intiGUI(quadrantView);
}
});
//start on sector (0, 0)
startingSector = startingQuadrant.getSectorArray()[0][0];
enterprise = new Enterprise(startingQuadrant, startingSector);
startingSector.setContainsEnterprise(true);
Scanner input = new Scanner(System.in);
Sector destinationSector;
int qRow; //destination quadrant row
int qCol; //destination quadrant column
int sRow; //destination sector row
int sCol; //destination sector column
while(true)
{
System.out.println("Enter quadrant row: ");
qRow = input.nextInt();
System.out.println("Enter quadrant column: ");
qCol = input.nextInt();
System.out.println("Enter sector row: ");
sRow = input.nextInt();
System.out.println("Enter sector column: ");
sCol = input.nextInt();
destinationSector = galaxy[qRow][qCol].getSectorArray()[sRow][sCol];
enterprise.move(destinationSector, galaxy[qRow][qCol], gui);
}
}
public GUI()
{
super("Star Trek");
//create an anonymous listener to close window and end game
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
dispose();
System.exit(0);
}
});
// get user's screen width and height
screenwidth = (int)Toolkit.getDefaultToolkit().getScreenSize().getWidth();
screenheight = (int)Toolkit.getDefaultToolkit().getScreenSize().getHeight();
//set layout
getContentPane().setLayout(new BorderLayout());
resizeGUI();
setVisible(true);
validate();
}
private void resizeGUI()
{
// set window size
if (screenwidth >= 1280)
setSize(1024, 768);
else if (screenwidth >= 1024)
setSize(800, 600);
else if (screenwidth >= 800)
setSize(640, 480);
// maximize window
setExtendedState(this.getExtendedState() | this.MAXIMIZED_BOTH);
}
//initialize this gui with the starting QuadrantView
public void intiGUI(QuadrantView quadrantView)
{
getContentPane().add(quadrantView, BorderLayout.CENTER);
validate();
}
//reset the gui to hold the new QuadrantView
public void resetGUI(QuadrantView newQuadrantView)
{
getContentPane().add(newQuadrantView, BorderLayout.CENTER);
validate();
}
static class Quadrant
{
private int row;
private int col;
private QuadrantView quadrantView;
private Sector[][] sectorArray;
public Quadrant(int r, int c)
{
// quadrant row
row = r;
// quadrant columns
col = c;
// the view object associated with this quadrant
setQuadrantView(new QuadrantView(8, 8));
// an array to hold the sectors in this quadrant (req. 3.1.0)
sectorArray = new Sector[8][8];
// create the 64 sectors in this quadrant and add them to the array (req. 3.1.0)
for (int i = 0; i < sectorArray.length; i ++)
{
for (int j = 0; j < sectorArray[i].length; j++)
{
sectorArray[i][j] = new Sector(i, j, this);
}
}
}
public int getRow()
{
return row;
}
public int getCol()
{
return col;
}
public void setRow(int r)
{
row = r;
}
public void setCol(int c)
{
col = c;
}
public Sector[][] getSectorArray()
{
return sectorArray;
}
public QuadrantView getQuadrantView()
{
return quadrantView;
}
public void setQuadrantView(QuadrantView quadrantView)
{
this.quadrantView = quadrantView;
}
}
static class Sector
{
//sector row
private int row;
//sector column
private int col;
//the quadrant this sector is in
private Quadrant quadrant;
//the view associated with this sector
private SectorView sectorView;
//boolean values to determine what this sector holds (Req. 4.1.0)
private boolean containsEnterprise;
//if the sector holds the Enterprise, store a reference to it
private Enterprise enterprise;
public Sector(int r, int c, Quadrant q)
{
row = r;
col = c;
quadrant = q;
setSectorView(new SectorView());
containsEnterprise = false;
//print the sector's coordinates on the gui
sectorView.setID(row + ", " + col);
}
public int getRow()
{
return row;
}
public int getCol()
{
return col;
}
public void setRow(int r)
{
row = r;
}
public void setCol(int c)
{
col = c;
}
public Quadrant getQuadrant()
{
return quadrant;
}
public boolean containsEnterprise()
{
return containsEnterprise;
}
public void setContainsEnterprise(boolean containsEnterprise)
{
this.containsEnterprise = containsEnterprise;
if (containsEnterprise)
{
sectorView.showEnterpriseIcon();
}
else
{
sectorView.hideEnterpriseIcon();
}
}
public Enterprise getEnterprise()
{
return enterprise;
}
public void addEnterprise(Enterprise enterprise)
{
this.enterprise = enterprise;
}
public void removeEnterprise()
{
enterprise = null;
}
public SectorView getSectorView()
{
return sectorView;
}
public void setSectorView(SectorView sectorView)
{
this.sectorView = sectorView;
}
public String toString()
{
return Integer.toString(row)+ "." + Integer.toString(col);
}
}
//end Sector class
static class SectorView extends JPanel
{
// default font for text
private final Font TREK_FONT = new Font("Verdana", Font.BOLD, 10);
// color for text
private final Color LABEL_COLOR = Color.BLACK;
// component layout
private SpringLayout layout;
// displays sector ID
private JLabel IDLabel;
//enterprise display
private JLabel enterpriseIcon;
/*
* create a new SectorView
*/
public SectorView()
{
super();
//create and set layout for child components
layout = new SpringLayout();
this.setLayout(layout);
//initialize child components
initComponents();
//position and display child components
layoutComponents();
//set background color
setBackground(Color.DARK_GRAY);
//set border
setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED));
//set size of sectors
setPreferredSize(new Dimension(QuadrantView.SECTOR_SIZE, QuadrantView.SECTOR_SIZE));
}
/*
* initialize components
*/
private void initComponents()
{
// displays ID of this view
IDLabel = new JLabel("");
IDLabel.setFont(TREK_FONT);
IDLabel.setForeground(Color.WHITE);
// create an enterprise icon and make it invisible
enterpriseIcon = new JLabel("E");
enterpriseIcon.setForeground(Color.WHITE);
enterpriseIcon.setVisible(false);
}
/*
* lay out components and add them to this view
*/
private void layoutComponents()
{
// position components:
// ID label
layout.putConstraint(SpringLayout.WEST, IDLabel, 1, SpringLayout.WEST, this);
layout.putConstraint(SpringLayout.NORTH, IDLabel, 1, SpringLayout.NORTH, this);
// enterprise icon
layout.putConstraint(SpringLayout.WEST, enterpriseIcon, 5, SpringLayout.WEST, this);
layout.putConstraint(SpringLayout.NORTH, enterpriseIcon, 30, SpringLayout.NORTH, this);
// add to view
this.add(IDLabel);
this.add(enterpriseIcon);
}
public void showEnterpriseIcon()
{
enterpriseIcon.setVisible(true);
}
public void hideEnterpriseIcon()
{
enterpriseIcon.setVisible(false);
}
//the sector's (row, col) coordinates within the quadrant
public void setID(String id)
{
IDLabel.setText(id);
}
}
//end SectorView class
static class QuadrantView extends JPanel
{
//size of sectors
public final static int SECTOR_SIZE = 100;
private final Color BACKGROUND_COLOR = Color.DARK_GRAY;
private SpringLayout layout;
/*
* create a new QuadrantView with the specified width
* and height
*
* #param quadrantHeight height of quad. in sectors
* #param quadrantWidth width of quad. in secors
*/
public QuadrantView(int quadrantHeight, int quadrantWidth)
{
//call JPanel constructor
super();
//create and set the layout
layout = new SpringLayout();
setLayout(layout);
//set the size of the QuadrantView we are creating using the inherited JComponent method
setPreferredSize(new Dimension(quadrantWidth * SECTOR_SIZE, quadrantHeight * SECTOR_SIZE));
//set background color using the inherited JComponent method
setBackground(BACKGROUND_COLOR);
}
/*
* add the specified Sector to this view
*
* each sector is represented by a (row, column) pair
* #param sectorView SectorView to be added to the QuadrantView
* #param row row coordinate
* #param col column coordinate
*/
public void addSectorView(SectorView sectorView, int row, int col)
{
//position the sector
layout.putConstraint(SpringLayout.WEST, sectorView, col * SECTOR_SIZE, SpringLayout.WEST, this);
layout.putConstraint(SpringLayout.NORTH, sectorView, row * SECTOR_SIZE, SpringLayout.NORTH, this);
//add sectorView to the layout using inherited method of Container class
this.add(sectorView);
}
}
static class Enterprise
{
protected Sector sectorLocation;
protected Quadrant quadrantLocation;
public Enterprise(Quadrant quadrant, Sector sector)
{
sectorLocation = sector;
quadrantLocation = quadrant;
sector.addEnterprise(this);
sector.setContainsEnterprise(true);
}
// Requirement 9.4.0
public boolean move(Sector destinationSector, final Quadrant destinationQuadrant, final GUI gui)
{
//if the destination quadrant is not our current quadrant, we need to update the gui (is updating this way causing a problem?)
if (!destinationQuadrant.equals(this.quadrantLocation))
{
//Put the new SectorViews in the new quadrant.
for (int i = 0; i < 8; i++)
{
for(int j = 0; j < 8; j++)
{
destinationQuadrant.getQuadrantView().addSectorView(destinationQuadrant.getSectorArray()[i][j].getSectorView(), i, j);
}
}
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
//initialize gui with the starting quadrant quadrantView
//replace the old quadrant view with the new one
gui.resetGUI(destinationQuadrant.getQuadrantView());
}
});
}
//remove the reference to this starship from the current sector
sectorLocation.removeEnterprise();
//sector no longer contains the Enterprise
sectorLocation.setContainsEnterprise(false);
//move to destination quadrant
quadrantLocation = destinationQuadrant;
//move to destination sector
sectorLocation = destinationSector;
//add a reference to this starship to the new sector
sectorLocation.addEnterprise(this);
//new sector now contains Enterprise
sectorLocation.setContainsEnterprise(true);
return true;
}
}//end Enterprise class
}
Limit the amount of data exchanged between threads. The only data which needs to be exchanged is the input from keyboard. Especially avoid sharing fields between threads - this leads to race conditions. Your main loop should look like this:
while(true)
{
final int qRow = input.nextInt();
final int qCol = input.nextInt();
final int sRow = input.nextInt();
final int sCol = input.nextInt();
SwingUtilities.invokeAndWait(new Runnable() {
#Override
public void run() {
move(qRow,qCol,sRow,sCol);
}
});
}
Remove all other invokeAndWait and invokeLater. Do not use invokeLater at all. It makes your program unpredictable.
Try to declare variables right before they are initialized and mark them as final. Mutable state leads to bugs.
I was not able to figure out why the table shrinks to 1x1. Try using GridLayout instead of SpringLayout. It seems better suited for this scenario.