简体   繁体   中英

repaint() doesn't work outside of keylistener

Over the past week or so I've been working on a game, where you're a white square that tries to dodge red squares which fall from the top of the screen. You can only move left and right, there are only 3 available positions to move to (left, middle, right), and the red square begins to fall faster as the game progresses. When I move the player (white square) everything repaints just fine, but when the red square's position updates it doesn't repaint at all. Say the red square moves once every second; if I wait one second, the repaint doesn't do anything. However, if I wait one second (or any amount of time) and then move the player, both squares jump to their new positions at once. After looking through the code for an hour, I still can't find any issues with it, and it doesn't help that I'm very new to coding, which probably means it's a really obvious mistake.

import java.util.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JComponent;
import javax.swing.JFrame;

public class boxface extends JComponent implements KeyListener {

  private static int x=0, y=0;
  static int counter = 0;
  static Thread t = new Thread();

  public void keyPressed(KeyEvent e) {
    if(e.getKeyCode()== KeyEvent.VK_RIGHT)
      moveRight();
    else if(e.getKeyCode()== KeyEvent.VK_LEFT)
      moveLeft(); }
  public void keyReleased(KeyEvent e) {}
  public void keyTyped(KeyEvent e) {}

  static Rectangle player = new Rectangle(x, 400, 50, 50);
  Rectangle bg = new Rectangle(0, 0, 700, 750);
  static int x2 = ((int)((Math.random()*3)))*50;
  static Rectangle enemy = new Rectangle(x2, y, 50, 50);

  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g;
    g2.setColor(Color.BLACK);
    g2.fill(bg);
    g2.setColor(Color.WHITE);
    g2.fill(player);
    g2.setColor(Color.RED);
    g2.fill(enemy); }

  public void moveLeft() {
    if(x > 0) {
      x -= 50;
      player.setLocation(x, 400);
      repaint();}}

  public void moveRight() {
    if(x < 100) {
      x += 50;
      player.setLocation(x, 400);
      repaint();} }

  public void enemyDown() {
    if(enemy.getBounds().y == 400) {
      x2=((int)((Math.random()*3)))*50;
      y=0;
      enemy.setLocation(x2, y);
      counter++;
      repaint(); }
    else {
      y = y + 50;
      enemy.setLocation(x2, y);
      repaint();}}

  public boxface(){
    addKeyListener(this);
    setFocusable(true);
    setFocusTraversalKeysEnabled(false); }

  public static void main(String[] args) {
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setBounds(400, 200, 156, 479);
        f.setMinimumSize(new Dimension(156, 0));
        f.setResizable(false);
        f.getContentPane().add(new boxface());
        f.setVisible(true); }
    });

    boxface exec = new boxface();
    int t = 20;

    for(long i = 0; i < 21; i++) {
      if(i == t) {
        exec.enemyDown();
        i = 0; }
      if(counter == 10) t = 15;
      if(counter == 25) t = 10;
      if(counter == 50) t = 7;
      if(counter == 100) t = 5;
      if(counter == 150) t = 3;
      if(counter == 225) t = 1;
      if(enemy.intersects(player))
        break;
      System.out.println(i); }
  }
}

Swing uses a "passive rendering" algorithm. This means that the UI will be updated at irregular intervals based on decisions been made by the RepaintManager

See Painting in AWT and Swing for more details

What you need is some way to schedule regular updates. Before I discuss that, some basic game theory...

Game/Main Loop...

Most games have a concept of a "main loop". The "main loop" does a number of jobs, it:

  • Updates the state of the game. This includes:
    • Updating the position of the player based on the current state of the input (keyboard/mouse/joystick)
    • Updating the position of other objects/obstacles within the world
    • Collision detection
    • Other states which need to be updated before they can be rendered
  • Rendering the current state

While you could use a Thread to accomplish this, Swing is NOT thread safe, meaning, you should never update the state of the UI (or any state that the UI relies on) outside the context of the Event Dispatching Thread. Swing is also single threaded, so you can't simply use Thread.sleep or some other loop within the context of the Event Dispatching Thread either.

The simplest solution would be to use a Swing Timer . This is pseudo loop, which is called repeatedly after a specified delay. Each time the timer is triggered, you'd perform the "main loop" operations and call repaint , thus, schedule a (mostly) regular update to the UI.

See How to use Swing Timers for more details

Other considerations...

Because the state of the game is actually updated within the "main loop", you can no longer respond to key events directly. Instead, you need to set a series of flags indicating the current state of your input triggers (ie isAPressed at its simplest form).

In most cases, this would invoke a Set and predefined series of inputs. I like to use a enum as it clearly defines the accepted inputs (up/down/left/right etc...). When pressed, you add the appropriate input to the Set and on release, you remove it. It deals with any repeated states and removes the oddity of the delay between the first press and the repeated key events.

Talking about key events. KeyListener has known issues, while there are "hacks" to "work around" it, they are just that, "hacks".

The recommend method for monitoring for (limited) key input is to use the key bindings API

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