I've been constructing a short program that basically draws a spaceship on a JPanel and listens for keys that tell the program to shoot a bullet. The problem is that it's not even painting the spaceship or the bullets on the screen. I also suspect that the KeyBindings may not be working as that was a previous problem (that I may or may not have fixed), but the main issue at hand is still the fact that my screen isn't being painted. Here is my code:
public enum Direction {
LEFT, RIGHT, SPACE
}
import javax.swing.JFrame;
public class Main {
public static void main(String[] args) {
JFrame frame;
Ship s1;
Shoot shoot;
// Set the frame up
frame = new JFrame();
frame.setSize(400, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
frame.setVisible(true);
// Get some more necessary objects
s1 = new Ship();
shoot = new Shoot(s1);
frame.getContentPane().add(shoot);
s1.setShoot(shoot);
// Threads
Thread ship = new Thread(s1);
ship.start();
}
}
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
public class Shoot extends JPanel {
Ship s1;
public Shoot(Ship s1) {
this.s1 = s1;
addKeyBinding(KeyEvent.VK_LEFT, "left.pressed", new MoveAction(true, s1, Direction.LEFT), true);
addKeyBinding(KeyEvent.VK_LEFT, "left.released", new MoveAction(false, s1, Direction.LEFT), false);
addKeyBinding(KeyEvent.VK_RIGHT, "right.pressed", new MoveAction(true, s1, Direction.RIGHT), true);
addKeyBinding(KeyEvent.VK_RIGHT, "right.released", new MoveAction(false, s1, Direction.RIGHT), false);
addKeyBinding(KeyEvent.VK_SPACE, "space.pressed", new MoveAction(true, s1, Direction.SPACE), true);
addKeyBinding(KeyEvent.VK_SPACE, "space.released", new MoveAction(false, s1, Direction.SPACE), false);
setDoubleBuffered(true);
}
#Override
public void paintComponent(Graphics g) {
// Draw the ship
super.paintComponent(g);
s1.draw(g);
g.fill3DRect(40, 50, 10, 10, false);
}
protected void addKeyBinding(int keyCode, String name, Action action, boolean keyPressed) {
if (keyPressed) {
addKeyBinding(KeyStroke.getKeyStroke(keyCode, 0, false), name, action);
} else {
addKeyBinding(KeyStroke.getKeyStroke(keyCode, 0, true), name, action);
}
}
protected void addKeyBinding(KeyStroke keyStroke, String name, Action action) {
InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
inputMap.put(keyStroke, name);
actionMap.put(name, action);
}
}
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
public class Ship implements Runnable {
int x, y, xDirection, bx, by;
boolean readyToFire, shooting = false;
Rectangle bullet;
Shoot shoot;
public Ship() {
x = 175;
y = 275;
bullet = new Rectangle(0, 0, 3, 5);
}
public void draw(Graphics g) {
// System.out.println("draw() called");
g.setColor(Color.BLUE);
g.fillRect(x, y, 40, 10);
g.fillRect(x + 18, y - 7, 4, 7);
if (shooting) {
g.setColor(Color.RED);
g.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
}
shoot.repaint();
}
public void move() {
x += xDirection;
if (x <= 0)
x = 0;
if (x >= 360)
x = 360;
shoot.repaint();
}
public void shoot() {
if (shooting) {
bullet.y--;
shoot.repaint();
}
}
public void setXDirection(int xdir) {
xDirection = xdir;
}
public void setShoot(Shoot shoot) {
this.shoot = shoot;
}
#Override
public void run() {
try {
while (true) {
shoot();
move();
Thread.sleep(5);
}
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
}
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.util.HashSet;
import javax.swing.AbstractAction;
public class MoveAction extends AbstractAction {
boolean pressed;
Ship s1;
Direction dir;
private Set<Direction> movement;
public MoveAction(boolean pressed, Ship s1, Direction dir) {
System.out.println("moveaction class");
movement = new HashSet<Direction>();
this.pressed = pressed;
this.s1 = s1;
this.dir = dir;
}
#Override
public void actionPerformed(ActionEvent e) {
try {
if (movement.contains(Direction.LEFT)) {
if (pressed) {
s1.setXDirection(-1);
} else {
s1.setXDirection(0);
}
} else if (movement.contains(Direction.RIGHT)) {
if (pressed) {
s1.setXDirection(1);
} else {
s1.setXDirection(0);
}
} else if (movement.contains(Direction.SPACE)) {
if (pressed) {
if (s1.bullet == null)
s1.readyToFire = true;
if (s1.readyToFire) {
s1.bullet.x = s1.x + 18;
s1.bullet.y = s1.y - 7;
s1.shooting = true;
}
} else {
s1.readyToFire = false;
if (s1.bullet.y <= -7) {
s1.bullet = null;
s1.shooting = false;
s1.bullet = null;
s1.bullet = new Rectangle(0, 0, 0, 0);
s1.readyToFire = true;
}
}
}
} catch (NullPointerException ex) {
System.out.println("NullPointerException");
}
}
So, there are a number of issues...
You should call setVisible on the JFrame last, this will ensure that components are laid out
Your keybindings issue seem to related to the fact that you're using the movement Set, but you never actually add anything to it. Instead you should be checking the dir value.
And probably a bunch of other things. Your code is tightly coupled and there isn't any centralised management of the state.
I'd start by having a better understand of the Model-View-Controller paradigm and separate the code into appropriate areas of responsibility.
The "data" for the game should separate from the "rendering" of the game, which should be separate from the decisions about how the game is to be updated.
What I'm about to present is an oversimplification intended to spark ideas, rather than been a concrete solution, as there are a number of ways you could achieve the physical implementation...
So, what we need is, a concept of something in the game, AKA an "entity", entities take many forms, but I'm going to focus on renderable/displayable entities. You need some kind of model that is responsible for modeling the current state of the game and which is responsible for implementing the rules. You need some kind of view, which is responsible for rendering the model and responding to user input. You need some kind of controller which controls how the model and the view bridged.
It's always a good idea to start with a series of interfaces which define the contractual expectations and outlines the expected means by which elements are expected to communicate with each other. Again, I've taken a simple, direct approach, but it's by no means the only...
public static enum Direction {
LEFT,
RIGHT,
SPACE
}
public interface Entity {
public void paint(Graphics2D g2d);
public Point getLocation();
public void setLocation(Point p);
public Dimension getSize();
}
public interface GameModel {
public Entity[] getEntities();
public void update(Rectangle bounds, Set<Direction> keys);
}
public interface GameController {
public Entity[] getEntities();
public void setDirection(Direction direction, boolean pressed);
public void start();
}
public interface GameView {
public void setController(GameController controller);
public GameController getController();
public Rectangle getViewBounds();
public void repaint();
}
Let's take a look at the implementation of the entities. This example has two entities, a Player and a Bullet...
public abstract class AbstractEntity implements Entity {
private final Point location = new Point(0, 0);
#Override
public Point getLocation() {
return new Point(location);
}
#Override
public void setLocation(Point p) {
location.setLocation(p);
}
}
public class Player extends AbstractEntity {
public Player(Rectangle bounds) {
int x = bounds.x + ((bounds.width - getSize().width) / 2);
int y = bounds.y + (bounds.height - getSize().height);
setLocation(new Point(x, y));
}
#Override
public Dimension getSize() {
return new Dimension(40, 17);
}
#Override
public void paint(Graphics2D g2d) {
Point p = getLocation();
Dimension size = getSize();
g2d.setColor(Color.BLUE);
g2d.fillRect(p.x, p.y + 7, size.width, 10);
g2d.fillRect(p.x + 18, p.y, 4, 7);
}
}
public class Bullet extends AbstractEntity {
#Override
public void paint(Graphics2D g2d) {
Rectangle bullet = new Rectangle(getLocation(), getSize());
g2d.setColor(Color.RED);
g2d.fill(bullet);
}
#Override
public Dimension getSize() {
return new Dimension(4, 8);
}
}
Nothing spectacular, but they each define their parameters and can render their states when asked.
Next, we have the model, controller and view. This example uses the controller as the primary game loop, responsible for updating the game (model) state and scheduling repaints. This is done with the use of a Swing Timer as this prevents possible race conditions between the update loop and the painting loop. A more complex implementation would need to take over the painting process through the use of a BufferStrategy and BufferStrategy and BufferCapabilities.
The model simply takes the current view boundaries and the current state of the keys and updates the state of the entities.
The view monitors user input, notifying the controller, and renders the current state of the game.
You'll note that the view and model never communicate directly with each other, this is the domain of the controller.
public class DefaultGameModel implements GameModel {
private final List<Entity> entities;
private Player player;
private Long lastShot;
public DefaultGameModel() {
entities = new ArrayList<>(25);
}
#Override
public Entity[] getEntities() {
return entities.toArray(new Entity[0]);
}
#Override
public void update(Rectangle bounds, Set<Direction> keys) {
if (player == null) {
player = new Player(bounds);
entities.add(player);
}
Point p = player.getLocation();
int xDelta = 0;
if (keys.contains(Direction.LEFT)) {
xDelta = -4;
} else if (keys.contains(Direction.RIGHT)) {
xDelta = 4;
}
p.x += xDelta;
if (p.x <= bounds.x) {
p.x = bounds.x;
} else if (p.x + player.getSize().width >= bounds.x + bounds.width) {
p.x = bounds.width - player.getSize().width;
}
player.setLocation(p);
Iterator<Entity> it = entities.iterator();
while (it.hasNext()) {
Entity entity = it.next();
if (entity instanceof Bullet) {
Point location = entity.getLocation();
Dimension size = entity.getSize();
location.y -= size.height;
if (location.y + size.height < bounds.y) {
it.remove();
} else {
entity.setLocation(location);
}
}
}
if (keys.contains(Direction.SPACE)) {
if (lastShot == null || System.currentTimeMillis() - lastShot > 100) {
lastShot = System.currentTimeMillis();
Bullet bullet = new Bullet();
int x = p.x + ((player.getSize().width - bullet.getSize().width) / 2);
int y = p.y - bullet.getSize().height;
bullet.setLocation(new Point(x, y));
entities.add(bullet);
}
}
}
}
public class DefaultGameController implements GameController {
private GameModel model;
private GameView view;
private Timer timer;
private Set<Direction> keys = new HashSet<>(25);
public DefaultGameController(GameModel gameModel, GameView gameView) {
gameView.setController(this);
view = gameView;
model = gameModel;
}
#Override
public Entity[] getEntities() {
return model.getEntities();
}
#Override
public void setDirection(Direction direction, boolean pressed) {
if (pressed) {
keys.add(direction);
} else {
keys.remove(direction);
}
}
#Override
public void start() {
if (timer != null && timer.isRunning()) {
timer.stop();
}
timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
model.update(view.getViewBounds(), Collections.unmodifiableSet(keys));
view.repaint();
}
});
timer.start();
}
}
public class DefaultGameView extends JPanel implements GameView {
private GameController controller;
public DefaultGameView() {
addKeyBinding("left.pressed", KeyEvent.VK_LEFT, true, new DirectionAction(Direction.LEFT, true));
addKeyBinding("left.released", KeyEvent.VK_LEFT, false, new DirectionAction(Direction.LEFT, false));
addKeyBinding("right.pressed", KeyEvent.VK_RIGHT, true, new DirectionAction(Direction.RIGHT, true));
addKeyBinding("right.released", KeyEvent.VK_RIGHT, false, new DirectionAction(Direction.RIGHT, false));
addKeyBinding("space.pressed", KeyEvent.VK_SPACE, true, new DirectionAction(Direction.SPACE, true));
addKeyBinding("space.released", KeyEvent.VK_SPACE, false, new DirectionAction(Direction.SPACE, false));
}
protected void addKeyBinding(String name, int keyEvent, boolean pressed, DirectionAction action) {
addKeyBinding(name, KeyStroke.getKeyStroke(keyEvent, 0, !pressed), action);
}
protected void addKeyBinding(String name, KeyStroke keyStroke, DirectionAction action) {
InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
inputMap.put(keyStroke, name);
actionMap.put(name, action);
}
#Override
public void setController(GameController controller) {
this.controller = controller;
}
#Override
public GameController getController() {
return controller;
}
#Override
public Rectangle getViewBounds() {
return new Rectangle(new Point(0, 0), getSize());
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
GameController controller = getController();
for (Entity entity : controller.getEntities()) {
// I don't trust you
Graphics2D g2d = (Graphics2D) g.create();
entity.paint(g2d);
g2d.dispose();
}
}
public class DirectionAction extends AbstractAction {
private Direction direction;
private boolean pressed;
public DirectionAction(Direction direction, boolean pressed) {
this.direction = direction;
this.pressed = pressed;
}
#Override
public void actionPerformed(ActionEvent e) {
getController().setDirection(direction, pressed);
}
}
}
Okay, but that's all fine and good, but how do you use it? Something like this for example...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
GameModel model = new DefaultGameModel();
DefaultGameView view = new DefaultGameView();
GameController controller = new DefaultGameController(model, view);
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(view);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
controller.start();
}
});
}
public static enum Direction {
LEFT,
RIGHT,
SPACE
}
public interface Entity {
public void paint(Graphics2D g2d);
public Point getLocation();
public void setLocation(Point p);
public Dimension getSize();
}
public interface GameModel {
public Entity[] getEntities();
public void update(Rectangle bounds, Set<Direction> keys);
}
public interface GameController {
public Entity[] getEntities();
public void setDirection(Direction direction, boolean pressed);
public void start();
}
public interface GameView {
public void setController(GameController controller);
public GameController getController();
public Rectangle getViewBounds();
public void repaint();
}
public class DefaultGameModel implements GameModel {
private final List<Entity> entities;
private Player player;
private Long lastShot;
public DefaultGameModel() {
entities = new ArrayList<>(25);
}
#Override
public Entity[] getEntities() {
return entities.toArray(new Entity[0]);
}
#Override
public void update(Rectangle bounds, Set<Direction> keys) {
if (player == null) {
player = new Player(bounds);
entities.add(player);
}
Point p = player.getLocation();
int xDelta = 0;
if (keys.contains(Direction.LEFT)) {
xDelta = -4;
} else if (keys.contains(Direction.RIGHT)) {
xDelta = 4;
}
p.x += xDelta;
if (p.x <= bounds.x) {
p.x = bounds.x;
} else if (p.x + player.getSize().width >= bounds.x + bounds.width) {
p.x = bounds.width - player.getSize().width;
}
player.setLocation(p);
Iterator<Entity> it = entities.iterator();
while (it.hasNext()) {
Entity entity = it.next();
if (entity instanceof Bullet) {
Point location = entity.getLocation();
Dimension size = entity.getSize();
location.y -= size.height;
if (location.y + size.height < bounds.y) {
it.remove();
} else {
entity.setLocation(location);
}
}
}
if (keys.contains(Direction.SPACE)) {
if (lastShot == null || System.currentTimeMillis() - lastShot > 100) {
lastShot = System.currentTimeMillis();
Bullet bullet = new Bullet();
int x = p.x + ((player.getSize().width - bullet.getSize().width) / 2);
int y = p.y - bullet.getSize().height;
bullet.setLocation(new Point(x, y));
entities.add(bullet);
}
}
}
}
public class DefaultGameController implements GameController {
private GameModel model;
private GameView view;
private Timer timer;
private Set<Direction> keys = new HashSet<>(25);
public DefaultGameController(GameModel gameModel, GameView gameView) {
gameView.setController(this);
view = gameView;
model = gameModel;
}
#Override
public Entity[] getEntities() {
return model.getEntities();
}
#Override
public void setDirection(Direction direction, boolean pressed) {
if (pressed) {
keys.add(direction);
} else {
keys.remove(direction);
}
}
#Override
public void start() {
if (timer != null && timer.isRunning()) {
timer.stop();
}
timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
model.update(view.getViewBounds(), Collections.unmodifiableSet(keys));
view.repaint();
}
});
timer.start();
}
}
public abstract class AbstractEntity implements Entity {
private final Point location = new Point(0, 0);
#Override
public Point getLocation() {
return new Point(location);
}
#Override
public void setLocation(Point p) {
location.setLocation(p);
}
}
public class Player extends AbstractEntity {
public Player(Rectangle bounds) {
int x = bounds.x + ((bounds.width - getSize().width) / 2);
int y = bounds.y + (bounds.height - getSize().height);
setLocation(new Point(x, y));
}
#Override
public Dimension getSize() {
return new Dimension(40, 17);
}
#Override
public void paint(Graphics2D g2d) {
Point p = getLocation();
Dimension size = getSize();
g2d.setColor(Color.BLUE);
g2d.fillRect(p.x, p.y + 7, size.width, 10);
g2d.fillRect(p.x + 18, p.y, 4, 7);
}
}
public class Bullet extends AbstractEntity {
#Override
public void paint(Graphics2D g2d) {
Rectangle bullet = new Rectangle(getLocation(), getSize());
g2d.setColor(Color.RED);
g2d.fill(bullet);
}
#Override
public Dimension getSize() {
return new Dimension(4, 8);
}
}
public class DefaultGameView extends JPanel implements GameView {
private GameController controller;
public DefaultGameView() {
addKeyBinding("left.pressed", KeyEvent.VK_LEFT, true, new DirectionAction(Direction.LEFT, true));
addKeyBinding("left.released", KeyEvent.VK_LEFT, false, new DirectionAction(Direction.LEFT, false));
addKeyBinding("right.pressed", KeyEvent.VK_RIGHT, true, new DirectionAction(Direction.RIGHT, true));
addKeyBinding("right.released", KeyEvent.VK_RIGHT, false, new DirectionAction(Direction.RIGHT, false));
addKeyBinding("space.pressed", KeyEvent.VK_SPACE, true, new DirectionAction(Direction.SPACE, true));
addKeyBinding("space.released", KeyEvent.VK_SPACE, false, new DirectionAction(Direction.SPACE, false));
}
protected void addKeyBinding(String name, int keyEvent, boolean pressed, DirectionAction action) {
addKeyBinding(name, KeyStroke.getKeyStroke(keyEvent, 0, !pressed), action);
}
protected void addKeyBinding(String name, KeyStroke keyStroke, DirectionAction action) {
InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
inputMap.put(keyStroke, name);
actionMap.put(name, action);
}
#Override
public void setController(GameController controller) {
this.controller = controller;
}
#Override
public GameController getController() {
return controller;
}
#Override
public Rectangle getViewBounds() {
return new Rectangle(new Point(0, 0), getSize());
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
GameController controller = getController();
for (Entity entity : controller.getEntities()) {
// I don't trust you
Graphics2D g2d = (Graphics2D) g.create();
entity.paint(g2d);
g2d.dispose();
}
}
public class DirectionAction extends AbstractAction {
private Direction direction;
private boolean pressed;
public DirectionAction(Direction direction, boolean pressed) {
this.direction = direction;
this.pressed = pressed;
}
#Override
public void actionPerformed(ActionEvent e) {
getController().setDirection(direction, pressed);
}
}
}
}
Again, you're going to need to go away and do some more research, but this is the general idea
Your drawing depends on the boolean variable shooting; there is one place where shooting is being set to true; if the key operations dont work the flow of the program may never reach there and that may never happen.
So I sugest you minimize your project to a screen that will draw the graphic without depending on pressing keys.
If you can see the graphics then you can add gradually the keys
Related
I'm new to java, and as a challenge I'm following some tutorials on how to create a small game. I've managed to make a nice keyboard input reader but sadly I don't know why my repaint(); isn't working... The point is to change the ints Py and Px, they are responsible in changing the position of the player!
This is the class f2, a JPanel that is added to a Jframe in another file.
import java.awt.*;
import java.awt.event.*;
public class f2 extends JPanel implements Runnable{
keys keyh = new keys();
Thread gameThreadMain;
//background color
Color bg2 = new Color(81,89,98);
Color txt1 = new Color(237,237,237);
//width control
final int w = 700, h = 700;
int wp = 64, hp = 64;
//spawn cords
//
int spawn1x = (w / 2) - (wp / 2);
int spawn1y = (h / 2) - (hp / 2);
//Player Speed
int Px = spawn1x;
int Py = spawn1y;
int Ps = 4;
//FPS
int fps = 60;
public f2(){
/* this.addKet*/
this.setBackground(bg2);
this.setPreferredSize(new Dimension(w,h));
this.setDoubleBuffered(true);
/*this.setLayout();*/
}
public void startGameThreadMain(){
gameThreadMain = new Thread(this);
gameThreadMain.start();
}
#Override
public void run(){
double drawInterval = 1000000000/fps;
double nextDrawTime = System.nanoTime() + drawInterval;
while(gameThreadMain != null){
update();
repaint();
/*System.out.print("check");*/
try{
double remainingTime = nextDrawTime - System.nanoTime();
remainingTime = remainingTime / 1000000;
if (remainingTime < 0) {
remainingTime = 0;
}
Thread.sleep((long) remainingTime);
nextDrawTime += drawInterval;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void update(){
//Keyboard
if(keyh.UpPressed == true) {
Py -= Ps;
}
else if(keyh.DownPressed == true) {
Py += Ps;
}
else if(keyh.LeftPressed == true) {
Px -= Ps;
}
else if(keyh.RightPressed == true) {
Px += Ps;
}else{
//
}
}
//player painter (this is the part where the 'repaint();' isn't working
public void paintComponent(Graphics g){
super.paintComponent(g);
Graphics2D player = (Graphics2D)g;
player.setColor(txt1);
player.fillRect(Px,Py,wp,hp);
System.out.println("repainted");
}
}```
//Thanks for reading!
startGameThreadMain is never called.
Swing is not thread AND is single threaded. You should never update the UI, or some state the UI depends on, from outside the context of the Event Dispatching Thread.
See Concurrency in Swing for more details and How to Use Swing Timers for one possible solution.
You should also be aware of:
How to Use Key Bindings
Painting in AWT and Swing
Performing Custom Painting
For example...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Player {
private int x;
private int y;
private Color backgroundColor = new Color(237, 237, 237);
private Shape shape;
public Player(int x, int y) {
this.x = x;
this.y = y;
this.shape = new Rectangle2D.Double(0, 0, getWidth(), getHeight());
}
public void setLocation(int x, int y) {
setX(x);
setY(y);
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getWidth() {
return 64;
}
public int getHeight() {
return 64;
}
public void paint(Graphics2D g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(backgroundColor);
g2d.translate(getX(), getY());
g2d.fill(shape);
g2d.dispose();
}
}
public enum Movement {
UP, DOWN, LEFT, RIGHT;
}
public interface MovementObserver {
public void movementDidActiviated(Object source, Movement movement);
public void movementWasDeactiviated(Object source, Movement movement);
}
public class TestPane extends JPanel {
private Timer timer;
private Player player;
private Set<Movement> movements = new HashSet<>();
public TestPane() {
player = new Player(0, 0);
int spawnY = (getPreferredSize().width - player.getWidth()) / 2;
int spawnX = (getPreferredSize().height - player.getHeight()) / 2;
player.setLocation(spawnX, spawnY);
setBackground(new Color(81, 89, 98));
timer = new Timer(15, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (movements.contains(Movement.UP)) {
player.setY(player.getY() - 1);
} else if (movements.contains(Movement.DOWN)) {
player.setY(player.getY() + 1);
}
if (movements.contains(Movement.LEFT)) {
player.setX(player.getX() - 1);
} else if (movements.contains(Movement.RIGHT)) {
player.setX(player.getX() + 1);
}
repaint();
}
});
MovementObserver observer = new MovementObserver() {
#Override
public void movementDidActiviated(Object source, Movement movement) {
movements.add(movement);
}
#Override
public void movementWasDeactiviated(Object source, Movement movement) {
movements.remove(movement);
}
};
InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap am = getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, false), "Up.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, false), "Down.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), "Left.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "Right.pressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), "Up.released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true), "Down.released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "Left.released");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "Right.released");
am.put("Up.pressed", new MovementAction(Movement.UP, observer, true));
am.put("Down.pressed", new MovementAction(Movement.DOWN, observer, true));
am.put("Left.pressed", new MovementAction(Movement.LEFT, observer, true));
am.put("Right.pressed", new MovementAction(Movement.RIGHT, observer, true));
am.put("Up.released", new MovementAction(Movement.UP, observer, false));
am.put("Down.released", new MovementAction(Movement.DOWN, observer, false));
am.put("Left.released", new MovementAction(Movement.LEFT, observer, false));
am.put("Right.released", new MovementAction(Movement.RIGHT, observer, false));
}
#Override
public Dimension getPreferredSize() {
return new Dimension(700, 700);
}
#Override
public void addNotify() {
super.addNotify();
if (timer == null) {
return;
}
timer.start();
}
#Override
public void removeNotify() {
super.removeNotify();
if (timer == null) {
return;
}
timer.stop();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (player == null) {
return;
}
Graphics2D g2d = (Graphics2D) g.create();
player.paint(g2d);
g2d.dispose();
}
}
public class MovementAction extends AbstractAction {
private Movement movement;
private MovementObserver observer;
private boolean activate;
public MovementAction(Movement movement, MovementObserver observer, boolean active) {
this.movement = movement;
this.observer = observer;
this.activate = active;
}
public Movement getMovement() {
return movement;
}
public MovementObserver getObserver() {
return observer;
}
public boolean doesActivate() {
return activate;
}
#Override
public void actionPerformed(ActionEvent e) {
if (doesActivate()) {
getObserver().movementDidActiviated(this, getMovement());
} else {
getObserver().movementWasDeactiviated(this, getMovement());
}
}
}
}
I am testing out graphics movement with simple action and key listener commands, but am struggling to incorporate the Graphics2D class. I am looking to draw an ellipse, which is then controllable through the use of the up, down, left and right keys. I am struggling to put all this together as i am very knew to a lot of this and followed a very outdated guide, as I could not find anything recent. Any help would be appreciated. Here is the code below:
import java.awt.*;
import java.awt.Graphics2D;
import javax.swing.JPanel;
import java.awt.event.*;
import javax.swing.Timer;
public class Screen extends JPanel implements ActionListener, KeyListener {
Timer t = new Timer(5, this);
int x = 0, y = 0, velx = 0, vely = 0;
public Screen() {
t.start();
addKeyListener(this);
setFocusable(true);
}
public void paintComponent(Graphics g)
{
super.paintComponent(g);
GRAPHICS2D g2 = (GRAPHICS2D) g;
g2.fill(new Ellipse2D.Double(x, y, 40, 40));
}
public void actionPeformed(ActionEvent e) {
repaint();
x += velx;
y += vely;
}
public void up() {
vely = -1.5;
velx = 0;
}
public void down(); {
vely = 1.5;
velx = 0;
}
public void left(); {
vely = 0;
velx = -1.5;
}
public void right(); {
vely = 0;
velx = 1.5;
}
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();
if (code == KeyEvent.VK_UP) {
up();
}
if (code == KeyEvent.VK_DOWN) {
down();
}
if (code == KeyEvent.VK_LEFT) {
left();
}
if (code == KeyEvent.VK_RIGHT) {
right();
}
}
public void keyTyped(KeyEvent e) {}
public void keyReleased(KeyEvent e) {}
}```
When ran, it gives an error saying it cannot find a symbol when drawing the Ellipse, as well as giving missing method body errors for all of the movement calls. Help would be appreciated!
You will want to become familiar with key bindings and actions it is going to seriously make your life simpler in the long run.
The following example is deliberately long winded, as it's intended to demonstrate the power of the Action API.
So, I started with a concept of "direction"
public enum Direction {
UP, DOWN,
LEFT, RIGHT;
}
This is what's going to be supported, in terms of movements.
I then setup a basic "direction action", which will add or remove a Direction to a Set. Through this workflow, the "main loop" will decide what to do based on what's in the Set
public class InputAction extends AbstractAction {
private Direction input;
private Set<Direction> manager;
private boolean release;
public InputAction(Direction input, Set<Direction> manager, boolean release) {
this.input = input;
this.manager = manager;
this.release = release;
}
#Override
public void actionPerformed(ActionEvent e) {
if (release) {
manager.remove(input);
} else {
manager.add(input);
}
}
}
Because it can become really confusing really fast, I created a "pressed" and "released" InputAction
public class InputPressedAction extends InputAction {
public InputPressedAction(Direction input, Set<Direction> manager) {
super(input, manager, false);
}
}
public class InputReleasedAction extends InputAction {
public InputReleasedAction(Direction input, Set<Direction> manager) {
super(input, manager, true);
}
}
Following from that, I created specific direction actions, again, to make it simpler to configure.
public class UpPressedAction extends InputPressedAction {
public UpPressedAction(Set<Direction> manager) {
super(Direction.UP, manager);
}
}
public class UpReleasedAction extends InputReleasedAction {
public UpReleasedAction(Set<Direction> manager) {
super(Direction.UP, manager);
}
}
public class DownPressedAction extends InputPressedAction {
public DownPressedAction(Set<Direction> manager) {
super(Direction.DOWN, manager);
}
}
public class DownReleasedAction extends InputReleasedAction {
public DownReleasedAction(Set<Direction> manager) {
super(Direction.DOWN, manager);
}
}
public class LeftPressedAction extends InputPressedAction {
public LeftPressedAction(Set<Direction> manager) {
super(Direction.LEFT, manager);
}
}
public class LeftReleasedAction extends InputReleasedAction {
public LeftReleasedAction(Set<Direction> manager) {
super(Direction.LEFT, manager);
}
}
public class RightPressedAction extends InputPressedAction {
public RightPressedAction(Set<Direction> manager) {
super(Direction.RIGHT, manager);
}
}
public class RightReleasedAction extends InputReleasedAction {
public RightReleasedAction(Set<Direction> manager) {
super(Direction.RIGHT, manager);
}
}
Next, I setup the key bindings...
InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), ActionKey.UP_PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), ActionKey.UP_RELEASED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), ActionKey.DOWN_PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), ActionKey.DOWN_RELEASED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), ActionKey.LEFT_PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), ActionKey.LEFT_RELEASED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), ActionKey.RIGHT_PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), ActionKey.RIGHT_RELEASED);
actionMap.put(ActionKey.UP_PRESSED, new UpPressedAction(actions));
actionMap.put(ActionKey.UP_RELEASED, new UpReleasedAction(actions));
actionMap.put(ActionKey.DOWN_PRESSED, new DownPressedAction(actions));
actionMap.put(ActionKey.DOWN_RELEASED, new DownReleasedAction(actions));
actionMap.put(ActionKey.LEFT_PRESSED, new LeftPressedAction(actions));
actionMap.put(ActionKey.LEFT_RELEASED, new LeftReleasedAction(actions));
actionMap.put(ActionKey.RIGHT_PRESSED, new RightPressedAction(actions));
actionMap.put(ActionKey.RIGHT_RELEASED, new RightReleasedAction(actions));
And you can start to see why I created all those action classes.
Then the "main" loop just needs to inspect the state of the Set and make determinations about what it should do...
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (actions.contains(Direction.UP)) {
YPos -= 1;
}
if (actions.contains(Direction.DOWN)) {
YPos += 1;
}
if (actions.contains(Direction.LEFT)) {
xPos -= 1;
}
if (actions.contains(Direction.RIGHT)) {
xPos += 1;
}
if (YPos < 0) {
YPos = 0;
} else if (YPos > getHeight() - shape.getBounds().height) {
YPos = getHeight() - shape.getBounds().height;
}
if (xPos < 0) {
xPos = 0;
} else if (xPos > getWidth() - shape.getBounds().width) {
xPos = getWidth() - shape.getBounds().width;
}
repaint();
}
});
And painting becomes as simple as moving the shape...
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.translate(xPos, YPos);
g2d.fill(shape);
g2d.dispose();
}
Runnable Example
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.geom.Ellipse2D;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public enum Direction {
UP, DOWN,
LEFT, RIGHT;
}
public class InputAction extends AbstractAction {
private Direction input;
private Set<Direction> manager;
private boolean release;
public InputAction(Direction input, Set<Direction> manager, boolean release) {
this.input = input;
this.manager = manager;
this.release = release;
}
#Override
public void actionPerformed(ActionEvent e) {
if (release) {
manager.remove(input);
} else {
manager.add(input);
}
}
}
public class InputPressedAction extends InputAction {
public InputPressedAction(Direction input, Set<Direction> manager) {
super(input, manager, false);
}
}
public class InputReleasedAction extends InputAction {
public InputReleasedAction(Direction input, Set<Direction> manager) {
super(input, manager, true);
}
}
public class UpPressedAction extends InputPressedAction {
public UpPressedAction(Set<Direction> manager) {
super(Direction.UP, manager);
}
}
public class UpReleasedAction extends InputReleasedAction {
public UpReleasedAction(Set<Direction> manager) {
super(Direction.UP, manager);
}
}
public class DownPressedAction extends InputPressedAction {
public DownPressedAction(Set<Direction> manager) {
super(Direction.DOWN, manager);
}
}
public class DownReleasedAction extends InputReleasedAction {
public DownReleasedAction(Set<Direction> manager) {
super(Direction.DOWN, manager);
}
}
public class LeftPressedAction extends InputPressedAction {
public LeftPressedAction(Set<Direction> manager) {
super(Direction.LEFT, manager);
}
}
public class LeftReleasedAction extends InputReleasedAction {
public LeftReleasedAction(Set<Direction> manager) {
super(Direction.LEFT, manager);
}
}
public class RightPressedAction extends InputPressedAction {
public RightPressedAction(Set<Direction> manager) {
super(Direction.RIGHT, manager);
}
}
public class RightReleasedAction extends InputReleasedAction {
public RightReleasedAction(Set<Direction> manager) {
super(Direction.RIGHT, manager);
}
}
public class TestPane extends JPanel {
enum ActionKey {
UP_PRESSED,
UP_RELEASED,
DOWN_PRESSED,
DOWN_RELEASED,
LEFT_PRESSED,
LEFT_RELEASED,
RIGHT_PRESSED,
RIGHT_RELEASED;
}
private Set<Direction> actions = new HashSet<>();
private Timer timer;
private Shape shape = new Ellipse2D.Double(0, 0, 40, 40);
int YPos = 0;
int xPos = 100 - 20;
public TestPane() {
InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), ActionKey.UP_PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), ActionKey.UP_RELEASED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), ActionKey.DOWN_PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), ActionKey.DOWN_RELEASED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), ActionKey.LEFT_PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), ActionKey.LEFT_RELEASED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), ActionKey.RIGHT_PRESSED);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), ActionKey.RIGHT_RELEASED);
actionMap.put(ActionKey.UP_PRESSED, new UpPressedAction(actions));
actionMap.put(ActionKey.UP_RELEASED, new UpReleasedAction(actions));
actionMap.put(ActionKey.DOWN_PRESSED, new DownPressedAction(actions));
actionMap.put(ActionKey.DOWN_RELEASED, new DownReleasedAction(actions));
actionMap.put(ActionKey.LEFT_PRESSED, new LeftPressedAction(actions));
actionMap.put(ActionKey.LEFT_RELEASED, new LeftReleasedAction(actions));
actionMap.put(ActionKey.RIGHT_PRESSED, new RightPressedAction(actions));
actionMap.put(ActionKey.RIGHT_RELEASED, new RightReleasedAction(actions));
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (actions.contains(Direction.UP)) {
YPos -= 1;
}
if (actions.contains(Direction.DOWN)) {
YPos += 1;
}
if (actions.contains(Direction.LEFT)) {
xPos -= 1;
}
if (actions.contains(Direction.RIGHT)) {
xPos += 1;
}
if (YPos < 0) {
YPos = 0;
} else if (YPos > getHeight() - shape.getBounds().height) {
YPos = getHeight() - shape.getBounds().height;
}
if (xPos < 0) {
xPos = 0;
} else if (xPos > getWidth() - shape.getBounds().width) {
xPos = getWidth() - shape.getBounds().width;
}
repaint();
}
});
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
public void addNotify() {
super.addNotify();
timer.start();
}
#Override
public void removeNotify() {
super.removeNotify();
timer.stop();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.translate(xPos, YPos);
g2d.fill(shape);
g2d.dispose();
}
}
}
Why not use a KeyListener
🤨
If you spend anytime researching the problem, you will find that, often (too often), KeyListener is "recommend" to monitor key input. Also, probably more often, you come across a number of issues related to the use of KeyListener, especially in this role.
KeyListener needs the component it's registered to be focusable AND have current focus before it will react to the user input.
The above solution also overcomes a OS limitation, where, when a key is first pressed, there is a short delay between the first and repeating keys. Instead, the above solution simply raises a flag when the key is pressed and removes it when it's released - simple, but effective
Introduction
MadProgrammer focused on Key Listeners in his answer. I want to focus on the model / view / controller (MVC) pattern and code organization.
Here's the GUI I came up with.
I use Key Bindings, so the arrow keys and the WASD keys move the circle. MadProgrammer explained it in his answer, so I won't repeat his explanation. As his key bindings do, mine move the circle at a steady pace as long as you're holding one of the keys down. Release the key, and the circle stops moving.
Explanation
Oracle has a helpful tutorial, Creating a GUI With Swing, that will show you the basics of Swing. Skip the Netbeans section.
In the MVC pattern, you create the model first, then the view, then the controllers. There's usually not one controller to rule them all. Each ActionListener acts independently, controlling its portion of the view and the model.
Model
The first thing I did was create the application model. I created two classes, CircleMovementModel and Circle. Both are plain Java getter / setter classes. The Circle class holds the components to draw a circle. The CircleMovementModel holds a Circle instance, the width, and the height of the drawing JPanel.
View
I started the Swing application 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 the JFrame and the drawing JPanel in separate classes. The JFrame has a default BorderLayout, which I used. I separate the creation of the JFrame and all associated JPanels into separate methods and classes so it's easier for human readers to understand the code.
The JFrame methods must be called in a specific order. This is the order I use for my Swing applications.
The drawing JPanel draws the circle in the specified position. Period. Nothing else. The movement of the circle is handled in the Swing Timer.
Controller
The key bindings look pretty complicated. There's a set of key bindings when the key is pressed. There's another set of key bindings when the key is released. The one Action class, MovementAction, handles all of the movements by passing the appropriate deltaX and deltaY through the constructor.
Finally, the ActionListener of the Swing Timer class moves the Circle instance at a steady pace. I made this class an anonymous class because it was so short.
Code
Here's the complete runnable code. I made all the additional classes inner classes so I could post this code as one block.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.geom.Ellipse2D;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class CircleMovementGUI implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new CircleMovementGUI());
}
private final CircleMovementModel model;
private final DrawingPanel drawingPanel;
public CircleMovementGUI() {
this.model = new CircleMovementModel();
this.drawingPanel = new DrawingPanel(model);
}
#Override
public void run() {
JFrame frame = new JFrame("Circle Movement");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(drawingPanel, BorderLayout.CENTER);
setKeyBindings(drawingPanel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
Timer timer = new Timer(20, new ActionListener() {
#Override
public void actionPerformed(ActionEvent event) {
model.getCircle().incrementX();
model.getCircle().incrementY();
drawingPanel.repaint();
}
});
timer.start();
}
private void setKeyBindings(DrawingPanel drawingPanel) {
InputMap inputMap = drawingPanel.getInputMap();
ActionMap actionMap = drawingPanel.getActionMap();
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "up");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "down");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "left");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "right");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, false), "up");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, false), "down");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), "left");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "right");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "stop");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "stop");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "stop");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "stop");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), "stop");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true), "stop");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "stop");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "stop");
actionMap.put("up", new MovementAction(model, 0.0, -1.5));
actionMap.put("down", new MovementAction(model, 0.0, +1.5));
actionMap.put("left", new MovementAction(model, -1.5, 0.0));
actionMap.put("right", new MovementAction(model, +1.5, 0.0));
actionMap.put("stop", new MovementAction(model, 0.0, 0.0));
}
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
private final CircleMovementModel model;
public DrawingPanel(CircleMovementModel model) {
this.model = model;
this.setBackground(Color.BLACK);
this.setPreferredSize(new Dimension(model.getDrawingPanelWidth(),
model.getDrawingPanelHeight()));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_ON);
Circle circle = model.getCircle();
int radius = circle.getRadius();
int diameter = radius + radius;
double x = Math.round(circle.getX()) - radius;
double y = Math.round(circle.getY()) - radius;
g2d.setColor(circle.getColor());
g2d.fill(new Ellipse2D.Double(x, y, diameter, diameter));
}
}
public class MovementAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private final double deltaX, deltaY;
private final CircleMovementModel model;
public MovementAction(CircleMovementModel model,
double deltaX, double deltaY) {
this.model = model;
this.deltaX = deltaX;
this.deltaY = deltaY;
}
#Override
public void actionPerformed(ActionEvent event) {
model.getCircle().setDeltaX(deltaX);
model.getCircle().setDeltaY(deltaY);
}
}
public class CircleMovementModel {
private final int drawingPanelWidth, drawingPanelHeight;
private final Circle circle;
public CircleMovementModel() {
this.drawingPanelWidth = 600;
this.drawingPanelHeight = drawingPanelWidth;
this.circle = new Circle(Color.YELLOW, 20, drawingPanelWidth / 2,
drawingPanelHeight / 2);
}
public int getDrawingPanelWidth() {
return drawingPanelWidth;
}
public int getDrawingPanelHeight() {
return drawingPanelHeight;
}
public Circle getCircle() {
return circle;
}
}
public class Circle {
private final int radius;
private double x, y, deltaX, deltaY;
private final Color color;
public Circle(Color color, int radius, int x, int y) {
this.color = color;
this.radius = radius;
this.x = x;
this.y = y;
this.deltaX = 0.0;
this.deltaY = 0.0;
}
public double getX() {
return x;
}
public void incrementX() {
this.x = x + deltaX;
}
public double getY() {
return y;
}
public void incrementY() {
this.y = y + deltaY;
}
public double getDeltaX() {
return deltaX;
}
public void setDeltaX(double deltaX) {
this.deltaX = deltaX;
}
public double getDeltaY() {
return deltaY;
}
public void setDeltaY(double deltaY) {
this.deltaY = deltaY;
}
public int getRadius() {
return radius;
}
public Color getColor() {
return color;
}
}
}
I have created a program that just moves a ball across a screen. I used to have it all in one class, but decided that it looked too messy so I split it up into three different classes: Main... initializes everything, Game... which paints everything and is a JPanel, and AL which is a KeyListener (which is also where the problem is). The problem is that I can't get the program to repaint from my AL class no matter what I try to pass into it. Can anyone help with this? Here are my three classes:
import java.awt.Color;
import javax.swing.JFrame;
public class Main {
static Game game;
static JFrame frame;
public static void main(String[] args) {
game = new Game();
frame = new JFrame();
frame.getContentPane().add(game);
frame.addKeyListener(new AL(game, frame));
frame.setTitle("Game");
frame.setSize(500, 500);
frame.setResizable(true);
frame.setVisible(true);
frame.setBackground(Color.BLACK);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
-
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Game extends JPanel implements Runnable {
int x, y, xCoord, yCoord;
private Image dbImage;
private Graphics dbg;
JFrame frame;
public void changeCoord() {
x += xCoord;
y += yCoord;
if (x <= 20) {
x = 20;
}
if (x >= 480) {
x = 480;
}
if (y <= 40) {
y = 40;
}
if (y >= 480) {
y = 480;
}
}
public void setXCoord(int xcoord) {
xCoord = xcoord;
}
public void setYCoord(int ycoord) {
yCoord = ycoord;
}
public static void main(String[] args) {
Game game = new Game();
Thread t = new Thread(game);
t.start();
}
public Game() {
x = 250;
y = 250;
}
#Override
public void paintComponent(Graphics g) {
g.setColor(Color.GREEN);
g.fillOval(x, y, 15, 15);
}
#Override
public void paint(Graphics g) {
dbImage = createImage(getWidth(), getHeight());
dbg = dbImage.getGraphics();
paintComponent(dbg);
g.drawImage(dbImage, 0, 0, this);
}
#Override
public void run() {
try {
while (true) {
changeCoord();
Thread.sleep(30);
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
-
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.JFrame;
public class AL extends KeyAdapter {
Game game;
JFrame frame;
public AL(Game game, JFrame frame) {
this.game = game;
this.frame = frame;
}
#Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == e.VK_LEFT) {
game.setXCoord(-1);
}
if (keyCode == e.VK_RIGHT) {
game.setXCoord(+1);
}
if (keyCode == e.VK_UP) {
game.setYCoord(-1);
}
if (keyCode == e.VK_DOWN) {
game.setYCoord(+1);
}
game.repaint();
}
#Override
public void keyReleased(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == e.VK_LEFT) {
game.setXCoord(0);
}
if (keyCode == e.VK_RIGHT) {
game.setXCoord(0);
}
if (keyCode == e.VK_UP) {
game.setYCoord(0);
}
if (keyCode == e.VK_DOWN) {
game.setYCoord(0);
}
game.repaint();
}
}
Let's start with the obvious....
This is problematic...
#Override
public void paintComponent(Graphics g) {
g.setColor(Color.GREEN);
g.fillOval(x, y, 15, 15);
}
#Override
public void paint(Graphics g) {
dbImage = createImage(getWidth(), getHeight());
dbg = dbImage.getGraphics();
paintComponent(dbg);
g.drawImage(dbImage, 0, 0, this);
}
There's no need to implement double buffering in Swing components, they already are. Also, you're breaking the painting contract, by not call the paint methods super methods
The whole thing should be...
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.GREEN);
g.fillOval(x, y, 15, 15);
}
See Painting in AWT and Swing and Performing Custom Painting for more details
KeyListener is well known for been problematic. It will only raise key events if the component it registered to is focuable AND has keyboard focus. A JPanel by default, is not focusable. Before you run of and try and make it focusable (and get bitterly disappointed), you should be using the Key Bindings API instead, which was designed to over come the limitations of KeyListener
As a basic example...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new Game());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Game extends JPanel {
int x, y, xCoord, yCoord;
public Game() {
x = 250;
y = 250;
addKeyBinding(KeyEvent.VK_LEFT, "move.left", new MoveAction(this, -1, 0));
addKeyBinding(KeyEvent.VK_RIGHT, "move.right", new MoveAction(this, 1, 0));
addKeyBinding(KeyEvent.VK_UP, "move.up", new MoveAction(this, 0, -1));
addKeyBinding(KeyEvent.VK_DOWN, "move.down", new MoveAction(this, 0, 1));
}
protected void addKeyBinding(int keyCode, String name, Action action) {
addKeyBinding(KeyStroke.getKeyStroke(keyCode, 0), name, action);
}
protected void addKeyBinding(KeyStroke keyStroke, String name, Action action) {
InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
inputMap.put(keyStroke, name);
actionMap.put(name, action);
}
public void changeCoord() {
x += xCoord;
y += yCoord;
if (x <= 20) {
x = 20;
}
if (x >= 480) {
x = 480;
}
if (y <= 40) {
y = 40;
}
if (y >= 480) {
y = 480;
}
repaint();
}
public void setXCoord(int xcoord) {
xCoord = xcoord;
changeCoord();
}
public void setYCoord(int ycoord) {
yCoord = ycoord;
changeCoord();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(480, 480);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.GREEN);
g.fillOval(x, y, 15, 15);
}
}
public class MoveAction extends AbstractAction {
private int xDelta;
private int yDelta;
// I'd prefer an interface with just the "move" methods, but
// that's more time I don't have
private Game game;
public MoveAction(Game game, int xDelta, int yDelta) {
this.xDelta = xDelta;
this.yDelta = yDelta;
this.game = game;
}
#Override
public void actionPerformed(ActionEvent e) {
game.setXCoord(xDelta);
game.setYCoord(yDelta);
}
}
}
But, wait, that isn't exactly what you want (trust me, I'm an anoymouse person on the Internet ;)), a better example might be...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new Game());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public enum Direction {
UP,
LEFT,
DOWN,
RIGHT;
}
public class Game extends JPanel {
int x, y, xCoord, yCoord;
private Set<Direction> movement;
public Game() {
x = 250;
y = 250;
movement = new HashSet<>(4);
addKeyPressedBinding(KeyEvent.VK_LEFT, "left.pressed", new MoveAction(movement, Direction.LEFT, true));
addKeyReleasedBinding(KeyEvent.VK_LEFT, "left.released", new MoveAction(movement, Direction.LEFT, false));
addKeyPressedBinding(KeyEvent.VK_RIGHT, "right.pressed", new MoveAction(movement, Direction.RIGHT, true));
addKeyReleasedBinding(KeyEvent.VK_RIGHT, "right.released", new MoveAction(movement, Direction.RIGHT, false));
addKeyPressedBinding(KeyEvent.VK_UP, "up.pressed", new MoveAction(movement, Direction.UP, true));
addKeyReleasedBinding(KeyEvent.VK_UP, "up.released", new MoveAction(movement, Direction.UP, false));
addKeyPressedBinding(KeyEvent.VK_DOWN, "down.pressed", new MoveAction(movement, Direction.DOWN, true));
addKeyReleasedBinding(KeyEvent.VK_DOWN, "down.released", new MoveAction(movement, Direction.DOWN, false));
Timer timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
changeCoord();
}
});
timer.start();
}
protected void addKeyBinding(int keyCode, String name, Action action) {
addKeyBinding(KeyStroke.getKeyStroke(keyCode, 0), name, action);
}
protected void addKeyPressedBinding(int keyCode, String name, Action action) {
addKeyBinding(KeyStroke.getKeyStroke(keyCode, 0, false), name, action);
}
protected void addKeyReleasedBinding(int keyCode, String name, Action action) {
addKeyBinding(KeyStroke.getKeyStroke(keyCode, 0, true), name, action);
}
protected void addKeyBinding(KeyStroke keyStroke, String name, Action action) {
InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
inputMap.put(keyStroke, name);
actionMap.put(name, action);
}
public void changeCoord() {
if (movement.contains(Direction.UP)) {
y--;
} else if (movement.contains(Direction.DOWN)) {
y++;
}
if (movement.contains(Direction.LEFT)) {
x--;
} else if (movement.contains(Direction.RIGHT)) {
x++;
}
x += xCoord;
y += yCoord;
if (x <= 20) {
x = 20;
}
if (x >= 480) {
x = 480;
}
if (y <= 40) {
y = 40;
}
if (y >= 480) {
y = 480;
}
repaint();
}
public void setXCoord(int xcoord) {
xCoord = xcoord;
changeCoord();
}
public void setYCoord(int ycoord) {
yCoord = ycoord;
changeCoord();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(480, 480);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.GREEN);
g.fillOval(x, y, 15, 15);
}
}
public class MoveAction extends AbstractAction {
private Set<Direction> movement;
private Direction direction;
private boolean pressed;
public MoveAction(Set<Direction> movement, Direction direction, boolean pressed) {
this.movement = movement;
this.direction = direction;
this.pressed = pressed;
}
#Override
public void actionPerformed(ActionEvent e) {
if (pressed) {
movement.add(direction);
} else {
movement.remove(direction);
}
}
}
}
What this does is simply activates a flag when a key is pressed (and deactivates it when it's released), then in a Swing Timer, we check which keys are "active" and update the location of the ball.
What this does is, eliminates the key "stutter" which is caused by the OS when a key is first pressed and held. A delay is inserted between the first key and the repeated key events. Instead, we just turn the flag on and off as we like.
It also allows you to move in two directions at the same time (horizontally and vertically)
Have a look at Concurrency in Swing and How to use Swing Timers for more details
In my program I try to paint on a JPanel when the mouse is pressed. The mousePressed method is just to test the painting from another class. Later on the spawn method will be called by other class methods. When I press the mouse button spawnPedestrian() is called, but no Pedestrian is painted. Below is a running example with code from my project. If you create a project Roundabout and paste this code in it, you should be able to run it (images are hotlinked).
How to fix the spawnPedestrian() method?
public class Roundabout extends JFrame {
public static Surface surface;
public Roundabout() {
initUI();
}
private void initUI() {
setTitle("Roundabout");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
surface = new Surface();
add(surface);
this.addMouseListener(new MouseAdapter() {// empty implementation of all
// MouseListener`s methods
#Override
public void mousePressed(MouseEvent e) {
//Spawn
Spawn sp = new Spawn();
sp.spawnPedestrian(300, 100);
}
});
setSize(1618, 850);
setLocationRelativeTo(null);
}
public static JPanel getSurface() {
return surface;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
Roundabout roundabout = new Roundabout();
roundabout.setVisible(true);
}
});
}
//Track class
class Track {
BufferedImage track;
Point trackPosition;
Point TRACK_POS = new Point(0, 0);
public Track() {
try {
track = ImageIO.read(new URL("http://i.stack.imgur.com/2U3j5.png"));
} catch (Exception ex) {
System.out.println("Problem loading track image: " + ex);
}
trackPosition = new Point(TRACK_POS.x, TRACK_POS.y);
}
public void paint(Graphics g) {
g.drawImage(track, TRACK_POS.x, TRACK_POS.y, null);
}
}
//Surface class
public class Surface extends JPanel {
Track track = new Track();
public List<Vehicle> toDraw = new ArrayList<>();
public Surface() {
Pedestrian p = new Pedestrian(100, 100);
toDraw.add(p);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
//setLayout(null);
track.paint(g);
//Make sure the track is painted first
for (Vehicle v : toDraw) {
v.paint(g);
}
}
}
class Pedestrian extends Vehicle {
BufferedImage pedestrian;
Point pedestrianPosition;
double pedestrianRotation = 0;
int pedestrianW, pedestrianH;
public Pedestrian(int x, int y) {
try {
pedestrian = ImageIO.read(new URL("http://i.stack.imgur.com/wm0I5.png"));
} catch (IOException e) {
System.out.println("Problem loading pedestrian images: " + e);
}
pedestrianPosition = new Point(x, y);
pedestrianW = pedestrian.getWidth();
pedestrianH = pedestrian.getHeight();
}
#Override
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.rotate(Math.toRadians(pedestrianRotation), pedestrianPosition.x, pedestrianPosition.y);
g2d.drawImage(pedestrian, pedestrianPosition.x, pedestrianPosition.y, null);
}
#Override
public void setPath(List<Point> path) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
#Override
public void update(double i) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
}
//Spawn class
class Spawn {
public void spawnPedestrian(int x, int y) {
//Create a new pedestrian.
System.out.println("Spawn a pedestrian.");
Pedestrian p = new Pedestrian(x, y);
Roundabout.surface.toDraw.add(p);
Roundabout.surface.revalidate();
Roundabout.surface.repaint();
}
}
public abstract class Vehicle {
public abstract void setPath(List<Point> path);
public abstract void update(double i);
public abstract void paint(Graphics g);
}
}
EDIT: It works now Pedestrian is spawned on mouse click.
Basically, you want to decouple your code and centralise the responsible to the classes. So the "data" should be maintained by a model of some kind, the rendering should be handle by some kind of view and the updates to the model and view should be handled by some kind of controller.
This makes it easier to swap out any one part with out requiring a whole bunch of new code or other changes. It also means that each class has a defined domain of responsibility and discourages you from trying to, for example, make changes to state from within the view which should be handled by the model (which could put the state into disarray)
Let's start with something that what's to be painted
public interface Sprite {
public void paint(Graphics2D g2d);
}
public interface MoveableSprite extends Sprite {
public void update(Container container);
}
These represent either a static sprite (like a tree for example) or a sprite which is moving (and wants to be updated on a regular bases)
These are contained within a model
public interface GameModel {
public List<Sprite> getSprites();
public void setObserver(Observer<MoveableSprite> observer);
public Observer<MoveableSprite> getObserver();
public void spawnSprite();
}
Which provides some means by which it can notify (in this case, a single) interested party about some kind of state change. For this example, that means a new MoveableSprite has become available
The Observer is pretty basic and just has a single call back...
public interface Observer<T> {
public void stateChanged(T parent);
}
And an "engine" to help drive it...
public class GameEngine {
private GameModel model;
private SurfacePane surface;
private Timer timer;
public GameEngine(GameModel model, SurfacePane surface) {
this.model = model;
this.surface = surface;
model.setObserver(new Observer<MoveableSprite>() {
#Override
public void stateChanged(MoveableSprite sprite) {
sprite.update(getSurface());
}
});
timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
for (Sprite sprite : getModel().getSprites()) {
if (sprite instanceof MoveableSprite) {
((MoveableSprite) sprite).update(getSurface());
}
}
getSurface().repaint();
}
});
}
public GameModel getModel() {
return model;
}
public SurfacePane getSurface() {
return surface;
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
}
This is a pretty basic example, but basically, it updates the position of MoveableSprite and asks the surface to repaint itself. It's also observing the GameModel for any new sprites and it will update their position immediately, so they don't appear in some "weird" place
Okay, now we actually need to implement some of this to make it work
public class DefaultGameModel implements GameModel {
private Observer<MoveableSprite> observer;
private List<Sprite> sprites;
public DefaultGameModel() {
sprites = new ArrayList<>(25);
for (int index = 0; index < 10; index++) {
spawnSprite();
}
}
#Override
public List<Sprite> getSprites() {
return Collections.unmodifiableList(sprites);
}
public void spawnSprite() {
try {
ZombieSprite sprite = new ZombieSprite();
sprites.add(sprite);
Observer<MoveableSprite> observer = getObserver();
if (observer != null) {
observer.stateChanged(sprite);
}
} catch (IOException e) {
e.printStackTrace();
}
}
#Override
public void setObserver(Observer<MoveableSprite> observer) {
this.observer = observer;
}
#Override
public Observer<MoveableSprite> getObserver() {
return observer;
}
}
public class ZombieSprite implements MoveableSprite {
private int x;
private int y;
private int xDelta;
private int yDelta;
private BufferedImage img;
private Observer<Sprite> observer;
private boolean initialised = false;
public ZombieSprite() throws IOException {
img = ImageIO.read(getClass().getResource("/LogoZombi.png"));
}
#Override
public void update(Container container) {
if (!initialised) {
x = (int) (Math.random() * container.getWidth());
y = (int) (Math.random() * container.getHeight());
Random rnd = new Random();
xDelta = rnd.nextBoolean() ? 1 : -1;
yDelta = rnd.nextBoolean() ? 1 : -1;
initialised = true;
}
x += xDelta;
y += yDelta;
if (x < 0) {
x = 0;
xDelta *= -1;
} else if (x + img.getWidth() > container.getWidth()) {
x = container.getWidth() - img.getWidth();
xDelta *= -1;
}
if (y < 0) {
y = 0;
yDelta *= -1;
} else if (y + img.getHeight() > container.getHeight()) {
y = container.getHeight() - img.getHeight();
yDelta *= -1;
}
}
#Override
public void paint(Graphics2D g2d) {
g2d.drawImage(img, x, y, null);
}
}
These two classes implement the GameModel and MoveableSprite interfaces. We use interfaces to decouple the code, which makes it easier to change the way in which things work and provides a jumping off point for agreed to contracts and exceptions of the implemenations
And finally, something that actually paints the current state...
public class SurfacePane extends JPanel {
private GameModel model;
public SurfacePane(GameModel model) {
this.model = model;
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
getModel().spawnSprite();
}
});
}
public GameModel getModel() {
return model;
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
GameModel model = getModel();
for (Sprite sprite : model.getSprites()) {
sprite.paint(g2d);
}
g2d.dispose();
}
}
You'll not that this class has the MouseListener, this is kind of deliberate, as other components which might be added to this container could prevent the MouseListener from been notified, so don't do that. But the MouseListener just calls the model to spawn another zombie...
And finally, we need to plumb it altogether...
GameModel model = new DefaultGameModel();
SurfacePane surfacePane = new SurfacePane(model);
GameEngine engine = new GameEngine(model, surfacePane);
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(surfacePane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
engine.start();
And just because I know that's a lot of disjointed concepts to put together, a complete example...
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeListener;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
GameModel model = new DefaultGameModel();
SurfacePane surfacePane = new SurfacePane(model);
GameEngine engine = new GameEngine(model, surfacePane);
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(surfacePane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
engine.start();
}
});
}
public class SurfacePane extends JPanel {
private GameModel model;
public SurfacePane(GameModel model) {
this.model = model;
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
getModel().spawnSprite();
}
});
}
public GameModel getModel() {
return model;
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
GameModel model = getModel();
for (Sprite sprite : model.getSprites()) {
sprite.paint(g2d);
}
g2d.dispose();
}
}
public class GameEngine {
private GameModel model;
private SurfacePane surface;
private Timer timer;
public GameEngine(GameModel model, SurfacePane surface) {
this.model = model;
this.surface = surface;
model.setObserver(new Observer<MoveableSprite>() {
#Override
public void stateChanged(MoveableSprite sprite) {
sprite.update(getSurface());
}
});
timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
for (Sprite sprite : getModel().getSprites()) {
if (sprite instanceof MoveableSprite) {
((MoveableSprite) sprite).update(getSurface());
}
}
getSurface().repaint();
}
});
}
public GameModel getModel() {
return model;
}
public SurfacePane getSurface() {
return surface;
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
}
public interface Observer<T> {
public void stateChanged(T parent);
}
public interface Sprite {
public void paint(Graphics2D g2d);
}
public interface MoveableSprite extends Sprite {
public void update(Container container);
}
public interface GameModel {
public List<Sprite> getSprites();
public void setObserver(Observer<MoveableSprite> observer);
public Observer<MoveableSprite> getObserver();
public void spawnSprite();
}
public class DefaultGameModel implements GameModel {
private Observer<MoveableSprite> observer;
private List<Sprite> sprites;
public DefaultGameModel() {
sprites = new ArrayList<>(25);
for (int index = 0; index < 10; index++) {
spawnSprite();
}
}
#Override
public List<Sprite> getSprites() {
return Collections.unmodifiableList(sprites);
}
public void spawnSprite() {
try {
ZombieSprite sprite = new ZombieSprite();
sprites.add(sprite);
Observer<MoveableSprite> observer = getObserver();
if (observer != null) {
observer.stateChanged(sprite);
}
} catch (IOException e) {
e.printStackTrace();
}
}
#Override
public void setObserver(Observer<MoveableSprite> observer) {
this.observer = observer;
}
#Override
public Observer<MoveableSprite> getObserver() {
return observer;
}
}
public class ZombieSprite implements MoveableSprite {
private int x;
private int y;
private int xDelta;
private int yDelta;
private BufferedImage img;
private Observer<Sprite> observer;
private boolean initialised = false;
public ZombieSprite() throws IOException {
img = ImageIO.read(getClass().getResource("/LogoZombi.png"));
}
#Override
public void update(Container container) {
if (!initialised) {
x = (int) (Math.random() * container.getWidth());
y = (int) (Math.random() * container.getHeight());
Random rnd = new Random();
xDelta = rnd.nextBoolean() ? 1 : -1;
yDelta = rnd.nextBoolean() ? 1 : -1;
initialised = true;
}
x += xDelta;
y += yDelta;
if (x < 0) {
x = 0;
xDelta *= -1;
} else if (x + img.getWidth() > container.getWidth()) {
x = container.getWidth() - img.getWidth();
xDelta *= -1;
}
if (y < 0) {
y = 0;
yDelta *= -1;
} else if (y + img.getHeight() > container.getHeight()) {
y = container.getHeight() - img.getHeight();
yDelta *= -1;
}
}
#Override
public void paint(Graphics2D g2d) {
g2d.drawImage(img, x, y, null);
}
}
}
I have these two classes:
public class Pencil extends JComponent implements MouseListener, MouseMotionListener{
Plansa plansa;
Graphics g;
public Pencil(Plansa newCanvas){
this.plansa = newCanvas;
this.plansa.setFlagShape(false);
}
#Override
public void mouseDragged(MouseEvent arg0) {
plansa.setMouseDragged(arg0);
this.plansa.setFlagShape(false);
plansa.paintComponent(plansa.getGraphics());
}
#Override
public void mouseClicked(MouseEvent e) {
}
#Override
public void mousePressed(MouseEvent arg0) {
plansa.setMousePressed(arg0);
}
#Override
public void mouseReleased(MouseEvent arg0) {
// TODO Auto-generated method stub
plansa.setMouseReleased(arg0);
this.plansa.setFlagShape(true);
plansa.paintComponent(plansa.getGraphics());
}
#Override
public void mouseEntered(MouseEvent e) {
}
#Override
public void mouseExited(MouseEvent e) {
}
#Override
public void mouseMoved(MouseEvent e) {
}
}
And this one:
public class Plansa extends JPanel{
Image image;
Pencil pencil;
//...
void init(){
this.setShape("freeLine");
this.setColor(Color.BLACK);
this.size=1;
this.type="round";
this.fill=false;
this.setBackground(Color.WHITE);
pencil = new Pencil(this);
addMouseListener(pencil);
addMouseMotionListener(pencil);
flagPaint = true;
flagShape = false;
}
public Plansa(){
this.setSize(800, 600);
init();
}
//...
#Override
public void paintComponent(Graphics g) {
g.setColor(currentColor);
Graphics2D g2d = (Graphics2D) g;
g2d.setStroke(setBrush(size,type));
super.paintComponent(g);
switch(shape){
default: break;
case "freeLine":{
g.drawLine(xDragged, yDragged, xCurrent, yCurrent);
break;
}
case "rectangle":{
if(flagShape == true){
g.drawRect(xPressed, yPressed, Math.max(xCurrent-xPressed,xPressed-xCurrent), Math.max(yCurrent-yPressed,yPressed-yCurrent));
if(fill == true) g.fillRect(xPressed, yPressed, Math.max(xCurrent-xPressed,xPressed-xCurrent), Math.max(yCurrent-yPressed,yPressed-yCurrent));
}
break;
}
case "circle":{
if(flagShape == true){
int radius = (int)Math.sqrt(Math.max(xCurrent-xPressed,xPressed-xCurrent)*Math.max(xCurrent-xPressed,xPressed-xCurrent)+Math.max(yCurrent-yPressed,yPressed-yCurrent)*Math.max(yCurrent-yPressed,yPressed-yCurrent));
g.drawOval(xPressed, yPressed, radius, radius);
if(fill == true) g.fillOval(xPressed, yPressed, radius, radius);
}
break;
}
case "oval":{
if(flagShape == true){
g.drawOval(xPressed, yPressed, Math.max(xCurrent-xPressed,xPressed-xCurrent), Math.max(yCurrent-yPressed,yPressed-yCurrent));
if(fill == true) g.fillOval(xPressed, yPressed, Math.max(xCurrent-xPressed,xPressed-xCurrent), Math.max(yCurrent-yPressed,yPressed-yCurrent));
}
break;
}
case "line":{
if(flagShape == true){
g.drawLine(xPressed, yPressed, xCurrent, yCurrent);
}
break;
}
}
}
//...
}
My problem is that every time I call paintComponent() method, the JPanel clears and the only item that remains is the one I just drawn. Is there any way to avoid this?
After close inspection of the code, it appears that you're trying to use Components as "painters", it's a bit like using a sports car as a go-kart, a lot of extras for little gain.
Instead, you should define yourself a interface of some kine that provides the basic requirements of what you want to draw/paint and then define concrete class implementations that implement the functionality.
You would then maintain a List of some kind of all the drawings that can be painted.
While simple, the example below provides a jumping off point that could be enhanced that allow selection of drawings, re-ordering and removal (should you want it).
This, is, essentially, the same concept proposed by Doorknob (+1)
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class TestDraw {
public static void main(String[] args) {
new TestDraw();
}
public TestDraw() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new Pencil());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Pencil extends JPanel implements MouseListener, MouseMotionListener {
private List<Drawable> drawables;
private Drawable activeDrawable;
private Point clickPoint;
public Pencil() {
drawables = new ArrayList<>(5);
addMouseListener(this);
addMouseMotionListener(this);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
for (Drawable drawable : drawables) {
drawable.paint(g2d);
}
g2d.dispose();
}
#Override
public void mouseDragged(MouseEvent e) {
if (activeDrawable != null) {
Point p = e.getPoint();
Rectangle bounds = activeDrawable.getBounds();
int x = bounds.x;
int y = bounds.y;
int width = p.x - clickPoint.x;
int height = p.y - clickPoint.y;
if (width < 0) {
width *= -1;
x = p.x;
}
if (height < 0) {
height *= -1;
y = p.y;
}
bounds = new Rectangle(x, y, width, height);
System.out.println(bounds);
activeDrawable.setBounds(bounds);
repaint();
}
}
#Override
public void mouseClicked(MouseEvent e) {
}
protected Drawable createActiveShape(MouseEvent e) {
System.out.println("Anchor = " + e.getPoint());
Drawable drawable = new FreeLine(e.getPoint());
drawable.setLocation(e.getPoint());
return drawable;
}
#Override
public void mousePressed(MouseEvent e) {
// You could also check to see if the clicked on a drawable...
clickPoint = e.getPoint();
activeDrawable = createActiveShape(e);
drawables.add(activeDrawable);
repaint();
}
#Override
public void mouseReleased(MouseEvent e) {
if (activeDrawable != null) {
Rectangle bounds = activeDrawable.getBounds();
if (bounds.width == 0 || bounds.height == 0) {
drawables.remove(activeDrawable);
}
}
clickPoint = null;
activeDrawable = null;
}
#Override
public void mouseEntered(MouseEvent e) {
}
#Override
public void mouseExited(MouseEvent e) {
}
#Override
public void mouseMoved(MouseEvent e) {
}
}
public interface Drawable {
public void setLocation(Point p);
public void setSize(Dimension dim);
public void setBounds(Rectangle bounds);
public Rectangle getBounds();
public void paint(Graphics2D g2d);
}
public abstract class AbstractDrawable implements Drawable {
private Rectangle bounds;
public AbstractDrawable() {
bounds = new Rectangle();
}
#Override
public void setLocation(Point p) {
bounds.setLocation(p);
}
#Override
public void setBounds(Rectangle bounds) {
this.bounds = bounds;
}
#Override
public void setSize(Dimension dim) {
bounds.setSize(dim);
}
#Override
public Rectangle getBounds() {
return bounds;
}
}
public class FreeLine extends AbstractDrawable {
private Point anchor;
public FreeLine(Point anchor) {
this.anchor = anchor;
}
#Override
public void paint(Graphics2D g2d) {
Rectangle bounds = getBounds();
Point p1 = new Point(anchor);
Point p2 = new Point(bounds.getLocation());
if (p1.x > p2.x) {
p2.x = p1.x - bounds.width;
} else {
p2.x = p1.x + bounds.width;
}
if (p1.y > p2.y) {
p2.y = p1.y - bounds.height;
} else {
p2.y = p1.y + bounds.height;
}
g2d.draw(new Line2D.Float(p1, p2));
}
}
Store all the objects in an ArrayList and iterate through it when you are drawing.
You could have them all implement a custom interface, Drawable for example, then store in an ArrayList<Drawable>.
A better idea generally for progressive drawings is to use a BufferedImage as the canvas, as seen here.
Don't use/comment out:
super.paintComponent(g);
That line is the one that is doing the clearing.