简体   繁体   中英

Java “Breakout” clone: Suspending and resuming thread

for an intro CS course I am attempting to make a clone of "Breakout" in Java. The game is 99% done, so I thought I'd add some extras.

One thing I wanted to add is an ability to pause and resume using the spacebar. I added a boolean "isPaused" variable and change it's value every time I call game.resume() and game.suspend(). Then, I use a KeyAdapter to tell the program to resume and pause based on the "isPaused" value when the user hits the spacebar. This seems to work most of the time, but occasionally two clicks of the spacebar are required. I've looked through the code extensively, and can't put a finger on the problem. It appears to happen usually when a new level starts. So, I'll post the code from the "Board.java" file, which contains the game logic and the problem at hand. Thanks! Code is below.

This "Board" class handles all game logic and displays items on the screen.

//imports
import java.awt.*;
import javax.swing.*;
import java.util.Random;
import java.lang.Thread;
import javax.sound.sampled.*;
import java.io.*;
import java.awt.event.*;
import java.util.ArrayList;

//class definition
public class Board extends JPanel implements Runnable, Constants {
//variables
Paddle paddle;
Ball ball;
Brick[][] brick = new Brick[10][5];
int score = 0, lives = 5, bricksLeft = 50, waitTime = 3, xSpeed, withSound, level = 1;
String playerName;
Thread game;
String songFile = "music/Start.wav";
Color brickColor = new Color(0,0,255);
ArrayList<Item> items = new ArrayList<Item>();
boolean isPaused = true;

//constructor
public Board(int width, int height) {
    super.setSize(width, height);
    addKeyListener(new BoardListener());
    setFocusable(true);

    makeBricks();
    paddle = new Paddle(width/2, height-(height/10), width/7, height/50, Color.BLACK);
    ball = new Ball(BALL_X_START, BALL_Y_START, BALL_WIDTH, BALL_HEIGHT, Color.BLACK);

    //Get the player's name
    playerName = JOptionPane.showInputDialog(null, "Enter your name:", "Name", JOptionPane.INFORMATION_MESSAGE);
    if (playerName == null) {
        System.exit(0);
    }

    //Start Screen that displays information and asks if the user wants music or not
    String[] options = {"Yes", "No"};
    withSound = JOptionPane.showOptionDialog(null, "Brick Breaker, Version 1.0\nBy Ty-Lucas Kelley, for CSC 171 Fall 2013\nAll credit for the music goes to the SEGA Corporation.\n\n\nControls: Press spacebar to start, and use the arrow keys to move.\n\n\nWould you like to play with the music on?", "Introduction", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[1]);
    playMusic(songFile, withSound);

    game = new Thread(this);
    game.start();
    stop();
}

//fills the array of bricks
public void makeBricks() {
    for(int i = 0; i < 10; i++) {
        for(int j = 0; j < 5; j++) {
            Random rand = new Random();
            int itemType = rand.nextInt(3) + 1;
            int numLives = 3;
            brick[i][j] = new Brick(i * BRICK_WIDTH, (j * BRICK_HEIGHT) + (BRICK_HEIGHT / 2), BRICK_WIDTH - 5, BRICK_HEIGHT - 4, brickColor, numLives, itemType);
        }
    }
}

//starts the thread
public void start() {
    game.resume();
    isPaused = false;
}

//stops the thread
public void stop() {
    game.suspend();
    isPaused = true;
}

//ends the thread
public void destroy() {
    game.resume();
    isPaused = false;
    game.stop();
}

//runs the game
public void run() {
    xSpeed = 1;
    while(true) {
        int x1 = ball.getX();
        int y1 = ball.getY();

        //Makes sure speed doesnt get too fast/slow
        if (Math.abs(xSpeed) > 1) {
            if (xSpeed > 1) {
                xSpeed--;
            }
            if (xSpeed < 1) {
                xSpeed++;
            }
        }

        checkPaddle(x1, y1);
        checkWall(x1, y1);
        checkBricks(x1, y1);
        checkLives();
        checkIfOut(y1);
        ball.move();
        dropItems();
        checkItemList();
        repaint();

        try {
            game.sleep(waitTime);
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        }
    }
}

public void addItem(Item i) {
    items.add(i);
}

public void dropItems() {
    for (int i = 0; i < items.size(); i++) {
        Item tempItem = items.get(i);
        tempItem.drop();
        items.set(i, tempItem);
    }
}

public void checkItemList() {
    for (int i = 0; i < items.size(); i++) {
        Item tempItem = items.get(i);
        if (paddle.caughtItem(tempItem)) {
            items.remove(i);
        }
        else if (tempItem.getY() > WINDOW_HEIGHT) {
            items.remove(i);
        }
    }
}

public void checkLives() {
    if (bricksLeft == 0) {
        ball.reset();
        bricksLeft = 50;
        makeBricks();
        lives++;
        level++;
        repaint();
        stop();
    }
    if (lives == 0) {
        repaint();
        stop();
    }
}

public void checkPaddle(int x1, int y1) {
    if (paddle.hitLeft(x1, y1)) {
        ball.setYDir(-1);
        xSpeed = -1;
        ball.setXDir(xSpeed);
    }
    else if (paddle.hitRight(x1, y1)) {
        ball.setYDir(-1);
        xSpeed = 1;
        ball.setXDir(xSpeed);
    }

    if (paddle.getX() <= 0) {
        paddle.setX(0);
    }
    if (paddle.getX() + paddle.getWidth() >= getWidth()) {
        paddle.setX(getWidth() - paddle.getWidth());
    }
}

public void checkWall(int x1, int y1) {
    if (x1 >= getWidth() - ball.getWidth()) {
        xSpeed = -Math.abs(xSpeed);
        ball.setXDir(xSpeed);
    }
    if (x1 <= 0) {
        xSpeed = Math.abs(xSpeed);
        ball.setXDir(xSpeed);
    }
    if (y1 <= 0) {
        ball.setYDir(1);
    }
    if (y1 >= getHeight()) {
        ball.setYDir(-1);
    }
}

public void checkBricks(int x1, int y1) {
    for (int i = 0; i < 10; i++) {
        for (int j = 0; j < 5; j++) {
            if (brick[i][j].hitBottom(x1, y1)) {
                ball.setYDir(1);
                if (brick[i][j].isDestroyed()) {
                    bricksLeft--;
                    score += 50;
                    addItem(brick[i][j].item);
                }
            }
            if (brick[i][j].hitLeft(x1, y1)) {
                xSpeed = -xSpeed;
                ball.setXDir(xSpeed);
                if (brick[i][j].isDestroyed()) {
                    bricksLeft--;
                    score += 50;
                    addItem(brick[i][j].item);
                }
            }
            if (brick[i][j].hitRight(x1, y1)) {
                xSpeed = -xSpeed;
                ball.setXDir(xSpeed);
                if (brick[i][j].isDestroyed()) {
                    bricksLeft--;
                    score += 50;
                    addItem(brick[i][j].item);
                }
            }
            if (brick[i][j].hitTop(x1, y1)) {
                ball.setYDir(-1);
                if (brick[i][j].isDestroyed()) {
                    bricksLeft--;
                    score += 50;
                    addItem(brick[i][j].item);
                }
            }
        }
    }
}

public void checkIfOut(int y1) {
    if (y1 > PADDLE_Y_START) {
        lives--;
        score -= 100;
        ball.reset();
        repaint();
        stop();
    }
}

//plays music throughout game if user wants to
public void playMusic(String song, int yesNo) {
    if (yesNo == 1) {
        return;
    }
    else if (yesNo == -1) {
        System.exit(0);
    }
    try {
        AudioInputStream audio = AudioSystem.getAudioInputStream(new File(song).getAbsoluteFile());
        Clip clip = AudioSystem.getClip();
        clip.open(audio);
        clip.loop(Clip.LOOP_CONTINUOUSLY); 
    } catch (Exception e) {
        e.printStackTrace();
    }
}

//fills the board
@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);
    paddle.draw(g);
    ball.draw(g);

    for (int i = 0; i < 10; i++) {
        for (int j = 0; j < 5; j++) {
            brick[i][j].draw(g);
        }
    }
    g.setColor(Color.BLACK);
    g.drawString("Lives: " + lives, 10, getHeight() - (getHeight()/10));
    g.drawString("Score: " + score, 10, getHeight() - (2*(getHeight()/10)) + 25);
    g.drawString("Level: " + level, 10, getHeight() - (3*(getHeight()/10)) + 50);
    g.drawString("Player: " + playerName, 10, getHeight() - (4*(getHeight()/10)) + 75);

    for (Item i: items) {
        i.draw(g);
    }

    if (lives == 0) {
        int heightBorder = getHeight()/10;
        int widthBorder = getWidth()/10;
        g.setColor(Color.BLACK);
        g.fillRect(widthBorder, heightBorder, getWidth() - (2 * widthBorder), getHeight() - (2 * heightBorder ));
        g.setColor(Color.WHITE);
        g.drawString("Game Over! Click the Spacebar twice to start over.", getWidth()/5, getHeight()/2);
    }
}

public String playerInfo() {
    return rank(score) + "." + " Name: " + playerName + ", Score: " + score;
}

public int rank(int score) {
    //check to see where this player falls on the list of saved games by reading from file
    return 0;
}

public void saveGame() {
    if (rank(score) >= 10) {
        return;
    }
    //save this game to HighScores.txt
}

public void printScores() {
    //print to paintComponent method. replace current 'game over' string
}

//Private class that handles gameplay and controls
private class BoardListener extends KeyAdapter {
    public void keyPressed(KeyEvent ke) {
        int key = ke.getKeyCode();
        if (key == KeyEvent.VK_SPACE) {
            if (lives > 0) {
                if (!isPaused) {
                    stop();
                }
                else {
                    start();
                }
            }
            else {
                paddle.setWidth(getWidth()/7);
                lives = 5;
                score = 0;
                bricksLeft = 50;
                level = 1;
                makeBricks();
                isPaused = true;
                for (int i = 0; i < 10; i++) {
                    for (int j = 0; j < 5; j++) {
                        brick[i][j].setDestroyed(false);
                    }
                }
            }
        }
        if (key == KeyEvent.VK_LEFT) {
            paddle.setX(paddle.getX() - 50);
        }
        if (key == KeyEvent.VK_RIGHT) {
            paddle.setX(paddle.getX() + 50);
        }
    }
    public void keyReleased(KeyEvent ke) {
        int key = ke.getKeyCode();
        if (key == KeyEvent.VK_LEFT) {
            paddle.setX(paddle.getX());
        }
        if (key == KeyEvent.VK_RIGHT) {
            paddle.setX(paddle.getX());
        }
    }
}

}

Your problem is most likely some kind of concurrency problem. You may want to switch your variable from boolean to AtomicBoolean and use synchronized (game) in your suspend and resume methods.

Also you should not use Thread.suspend and Thread.resume methods. Read their JavaDoc for more information.

Like this:

...
AtomicBoolean isPaused = new AtomicBoolean(false);
...

private void gamePause() {
   synchronized(game) {
      game.isPaused.set(true);
      game.notify();
   }
}

private void gameContinue() {
   synchronized(game) {
      game.isPaused.set(false);
      game.notify();
   }
}
...

Then in the places where you handle the loop:

...
public void run() {
  xSpeed = 1;
  while(true) {
    synchronized(game) {
      while(game.isPaused().get()) {
         try {
            Thread.sleep(1000);
         } catch (InterruptedException iex) {
            // This most likely means your JVM stops. Maybe log the Exception.
            game.destroy();
            return;
         }
      }
      int x1 = ball.getX();
      int y1 = ball.getY();
      ...
      }
    }
  }
}

And also in the checkLives method. (As far as I can see checkLives is only called from run, if thats the case this is already in a synchronized(game) block. If not, you have to add synchronized around stop() here as well.

public void checkLives() {
    if (bricksLeft == 0) {
        ...
        if(!game.isPaused().get())
           stop();
    }
    if (lives == 0) {
        repaint();
        if(!game.isPaused().get())
           stop();
    }
}

The problem is that checkLives() calls stop() which triggers isPaused to be flipped. If at the same time the KeyListener is activated it probes isPaused , thinks the game is paused and resumes it, thus you have to hit space again to continue.

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

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