I want to add a Scroll To End (Autoscroll) function to a jtable with maximum up to 50 rows.
AutoScroll function should work as IntelliJ Scroll To End.
Initially there are 0 rows. In several milliseconds (say 100 ms.) a few item (1-5 items) is added to the end of the table. If the table row count reaches to the max (50), the first row will be deleted before the next insertion.
When user scrolls to the end with mouse dragging or scroll wheel, scroll to end checkbox selected and function will be enabled. From that moment, the scrollpane should be scrolled to the bottom of the table like:
scrollRectToVisible(getCellRect(getRowCount() - 1, 0, true));
When user changes scrollbar position other than the bottom of viewport by dragging or mouse scroll wheel, scroll to end checkbox will deselected and function will be disabled.
While scroll to end checkbox is not selected, the table will react to scroll bar movements which are sourced by dragging or mouse wheel. But the viewport should show same data.
If it is impossible to show same data in the viewport because of deletions of the consequent first rows, the visible first data in the viewport will go to the up.
This is the exact behavior of the IntelliJ console log screen.
I wrote Scroll To End enable functionality (90% percent working).
I could not write Scroll To End disable functionality, I don't know to preserve current viewport as stated above.
Thanks for your help.
package org.example.table;
import com.bsbls.home.gui.test.GuiTester;
import javax.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import java.awt.*;
import java.awt.event.*;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
public class MyTable extends JTable {
JScrollPane scrollPane;
boolean autoScroll;
private Consumer<Boolean> scrollToEndListener;
private Object lastValue;
private ScheduledFuture<?> future;
public MyTable(int max) {
this.setFillsViewportHeight(true);
this.setRowHeight(24);
this.setModel(new MyTableModel(max));
this.addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent e) {
if (autoScroll) {
scrollToEnd();
}
}
});
}
public MyTableModel getTableModel() {
return (MyTableModel) getModel();
}
private void scrollToEnd() {
scrollRectToVisible(getCellRect(getRowCount() - 1, 0, true));
}
public void setScrollToEnd(boolean autoScroll) {
this.autoScroll = autoScroll;
if (autoScroll) {
scrollToEnd();
}
if (scrollToEndListener != null) {
scrollToEndListener.accept(autoScroll);
}
}
public void setScrollToEndListener(Consumer<Boolean> scrollToEndListener) {
this.scrollToEndListener = scrollToEndListener;
}
public JScrollPane wrap() {
if (scrollPane == null) {
scrollPane = new JScrollPane(this);
scrollPane.getViewport().setScrollMode(JViewport.BACKINGSTORE_SCROLL_MODE);
JScrollBar scrollBar = scrollPane.getVerticalScrollBar();
scrollBar.addAdjustmentListener(e -> {
if (e.getValue() + scrollBar.getVisibleAmount() == scrollBar.getMaximum()) {
setScrollToEnd(true);
} else {
setScrollToEnd(false);
}
});
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scrollPane.addMouseWheelListener(new MouseWheelListener() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (future != null) {
future.cancel(false);
}
future = scheduler.schedule(() -> {
EventQueue.invokeLater(() -> {
System.out.println("Yes");
JViewport viewport = scrollPane.getViewport();
Point p = viewport.getViewPosition();
Dimension extentSize = viewport.getExtentSize();
//p.translate(extentSize.width, extentSize.height);
int rowIndex = rowAtPoint(p);
if (rowIndex >= 0) {
lastValue = getValueAt(rowIndex, 0);
}
});
}, 500, TimeUnit.MILLISECONDS);
}
});
scrollBar.addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
//System.out.println(e);
}
#Override
public void mousePressed(MouseEvent e) {
//System.out.println(e);
lastValue = null;
}
#Override
public void mouseReleased(MouseEvent e) {
//System.out.println(e);
JViewport viewport = scrollPane.getViewport();
Point p = viewport.getViewPosition();
Dimension extentSize = viewport.getExtentSize();
//p.translate(extentSize.width, extentSize.height);
int rowIndex = rowAtPoint(p);
if (rowIndex >= 0) {
lastValue = getValueAt(rowIndex, 0);
}
}
#Override
public void mouseEntered(MouseEvent e) {
//System.out.println(e);
}
#Override
public void mouseExited(MouseEvent e) {
//System.out.println(e);
}
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
//System.out.println(e);
}
#Override
public void mouseDragged(MouseEvent e) {
// System.out.println(e);
}
#Override
public void mouseMoved(MouseEvent e) {
//System.out.println(e);
}
});
getTableModel().addTableModelListener(new TableModelListener() {
#Override
public void tableChanged(TableModelEvent e) {
if (lastValue != null && !autoScroll) {
int rowCount = getRowCount();
int newIndex = -1;
for (int i = 0; i < rowCount; i++) {
Object indexValue = getValueAt(i, 0);
if (indexValue == lastValue) {
newIndex = i;
break;
}
}
System.out.println(lastValue + " " + newIndex);
if (newIndex > 1) {
scrollRectToVisible(getCellRect(newIndex - 1, 0, true));
} else {
lastValue = null;
}
}
}
});
}
return scrollPane;
}
static int counter;
public static void main(String[] args) {
GuiTester.test(f -> {
JPanel panel = new JPanel(new BorderLayout());
MyTable table = new MyTable(50);
MyTableModel model = table.getTableModel();
Random random = new Random();
Timer timer = new Timer(100, e -> {
Data data = new Data();
data.setName(++counter + "");
data.setX(random.nextInt());
data.setY(random.nextInt());
data.setZ(random.nextInt());
data.setFlag(random.nextBoolean());
model.addRow(data.toObjectArray());
});
timer.start();
DataPanel dataPanel = new DataPanel();
table.getSelectionModel().addListSelectionListener(e -> {
int index = e.getFirstIndex();
if (index >= 0) {
Data data = (Data) table.getValueAt(index, 5);
dataPanel.setData(data);
}
});
JCheckBox checkBox = new JCheckBox("Scroll To End");
table.setScrollToEndListener(flag -> {
checkBox.setSelected(flag);
});
checkBox.addItemListener(e -> {
table.setScrollToEnd(checkBox.isSelected());
});
panel.add(checkBox, BorderLayout.NORTH);
panel.add(table.wrap(), BorderLayout.CENTER);
panel.add(dataPanel.getPanel(), BorderLayout.EAST);
return panel;
});
}
}
Table Model
package org.example.table;
import javax.swing.table.DefaultTableModel;
public class MyTableModel extends DefaultTableModel {
private int max = -1;
public MyTableModel() {
this(-1);
}
public MyTableModel(int max) {
super(new Object[]{
"Name", "Flag", "X", "Y", "Z", "Data"
}, 0);
this.max = max;
}
#Override
public void addRow(Object[] rowData) {
if (getRowCount() == max) {
super.removeRow(0);
}
super.addRow(rowData);
}
}
Dummy data
package org.example.table;
public class Data {
private String name;
private boolean flag;
private int x;
private int y;
private int z;
public Object[] toObjectArray() {
Object[] array = new Object[6];
array[0] = name;
array[1] = flag;
array[2] = x;
array[3] = y;
array[4] = z;
array[5] = this;
return array;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getZ() {
return z;
}
public void setZ(int z) {
this.z = z;
}
}
DatPanel which is a dummy IntelliJ form:
package org.example.table;
import javax.swing.*;
public class DataPanel {
private JTextField fieldName;
private JCheckBox flagCheckBox;
private JTextField fieldX;
private JTextField fieldY;
private JTextField fieldZ;
private JPanel panel;
public JPanel getPanel() {
return panel;
}
public void setData(Data data) {
fieldName.setText(data.getName());
fieldX.setText(data.getX() + "");
fieldY.setText(data.getY() + "");
fieldZ.setText(data.getZ() + " ");
flagCheckBox.setSelected(data.isFlag());
}
}
EDIT:
I have added to mouse and mouse wheel listeners, and find the first visible row.
And changed scroll mode to simple or backingstore.
scrollPane.getViewport().setScrollMode(JViewport.BACKINGSTORE_SCROLL_MODE);
In that case I have got a better function.
I am still open for better approaches.
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
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've been working the movement of this cube, however, its movement is pretty ugly and sudden, so is there anyway that I could make it "smooth" and "clean"?
Here is my code:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public class Main extends JPanel implements KeyListener
{
Environment environment = new Environment ();
Cube cube = new Cube ();
JFrame frame = new JFrame ();
int cubeX = cube.cube.x;
int cubeY = cube.cube.y;
// Paint method used to repaint the cube's location
public void paint (Graphics g)
{
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint (RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
environment.createBox (g2d);
cube.createCube (g2d);
}
// Getting pressed keys to move cube
#Override
public void keyPressed (KeyEvent e)
{
if (e.getKeyCode () == KeyEvent.VK_UP)
{
try
{
cube.isCubeMoving = true;
cube.moveCube ();
Thread.sleep (10);
frame.repaint ();
}
catch (InterruptedException ie)
{
ie.printStackTrace ();
}
}
else if (e.getKeyCode () == KeyEvent.VK_DOWN)
{
cube.cube.y = cube.cube.y + 100;
if (cube.cube.y > 620)
{
cube.cube.y = 620;
}
try
{
Thread.sleep (10);
frame.repaint ();
}
catch (InterruptedException e1)
{
e1.printStackTrace ();
}
}
}
#Override
public void keyReleased (KeyEvent arg0)
{
}
#Override
public void keyTyped (KeyEvent arg0)
{
}
// Main method
public static void main (String[] args) throws InterruptedException
{
Main m = new Main ();
m.frame.add (m);
m.frame.addKeyListener (m);
m.frame.setSize (700, 1000);
m.frame.setVisible (true);
m.frame.setTitle ("The Cube");
m.frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
m.frame.setResizable (true);
m.frame.setLocationRelativeTo (null);
m.frame.setBackground (new Color (240, 84, 84));
while (true)
{
m.frame.repaint ();
Thread.sleep (3);
}
}
}
Here's the Cube class:
import java.awt.*;
public class Cube extends Thread
{
public int x = 200;
public int y = 620;
public boolean isCubeMoving = true;
int whereCubeStops = 440;
Runnable r = new Runnable ()
{
public void run ()
{
while (isCubeMoving == true)
{
cube.setLocation (x, y -= 10);
System.out.println (y);
if (y == whereCubeStops)
{
try
{
isCubeMoving = false;
cube.setLocation (x, y = 620);
Thread.sleep (100);
}
catch (InterruptedException e)
{
e.printStackTrace ();
}
}
try
{
Thread.sleep (10);
}
catch (InterruptedException e)
{
e.printStackTrace ();
}
}
}
};
Rectangle cube = new Rectangle (x, y, 80, 80);
public void createCube (Graphics2D g2d)
{
g2d.setColor (new Color (148, 235, 148));
g2d.fill (cube);
}
public void moveCube ()
{
new Thread (r).start ();
}
}
Thanks very much for all your help!
:)
Well, for this, we have to write a proper game loop. Let's start one by one:
Here's my GameFrame:
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public abstract class GameFrame extends JFrame
{
private GamePanel gamePanel;
public GameFrame (String gameTitle, GamePanel gamePanel)
{
super (gameTitle);
this.gamePanel = gamePanel;
setDefaultCloseOperation (WindowConstants.EXIT_ON_CLOSE);
addWindowListener (new FrameListener ());
getContentPane ().setLayout (new GridBagLayout ());
getContentPane ().add (gamePanel);
pack ();
setLocationRelativeTo (null);
setResizable (false);
setVisible (true);
}
public class FrameListener extends WindowAdapter
{
public void windowActivated (WindowEvent event)
{
gamePanel.setWindowPaused (false);
}
public void windowDeactivated (WindowEvent event)
{
gamePanel.setWindowPaused (true);
}
public void windowDeiconified (WindowEvent event)
{
gamePanel.setWindowPaused (false);
}
public void windowIconified (WindowEvent event)
{
gamePanel.setWindowPaused (true);
}
public void windowClosing (WindowEvent event)
{
gamePanel.stopGame ();
}
}
}
Its an abstract class, and all it does is put a GamePanel in it, and make itself visible when initialised.
Here's my GamePanel which implements a game loop:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
public abstract class GamePanel extends JPanel implements Runnable
{
private int panelWidth;
private int panelHeight;
private Thread animator;
private volatile boolean running = false;
private volatile boolean isUserPaused = false;
private volatile boolean isWindowPaused = false;
private Graphics2D dbg;
private Image dbImage = null;
private static final int NO_DELAYS_PER_YIELD = 16;
private static final int MAX_FRAME_SKIPS = 5;
private Color backgroundColor;
private long period;
public GamePanel (int width, int height, long fps, Color backgroundColor)
{
this.panelWidth = width;
this.panelHeight = height;
this.backgroundColor = backgroundColor;
this.period = 1000000L * (long) 1000.0 / fps;
setBackground (backgroundColor);
setPreferredSize (new Dimension (panelWidth, panelHeight));
setFocusable (true);
requestFocus ();
readyForPause ();
addKeyListener (new KeyAdapter ()
{
public void keyPressed (KeyEvent e)
{
consumeKeyPressed (e.getKeyCode ());
}
});
}
protected abstract void consumeKeyPressed (int keyCode);
protected abstract void renderGame (Graphics2D graphics);
protected abstract void updateGame ();
#Override
public void addNotify ()
{
super.addNotify ();
startGame ();
}
protected void startGame ()
{
if (animator == null || ! running)
{
animator = new Thread (this);
animator.start ();
}
}
protected void stopGame ()
{
running = false;
}
private void readyForPause ()
{
addKeyListener (new KeyAdapter ()
{
public void keyPressed (KeyEvent e)
{
int keyCode = e.getKeyCode ();
if ((keyCode == KeyEvent.VK_ESCAPE) || (keyCode == KeyEvent.VK_Q)
|| (keyCode == KeyEvent.VK_END) || (keyCode == KeyEvent.VK_P)
|| ((keyCode == KeyEvent.VK_C) && e.isControlDown ()))
{
setUserPaused (! isUserPaused);
}
}
});
}
public void run ()
{
long beforeTime, afterTime, timeDiff, sleepTime;
long overSleepTime = 0L;
int noDelays = 0;
long excess = 0L;
beforeTime = System.nanoTime ();
running = true;
while (running)
{
requestFocus ();
gameUpdate ();
gameRender ();
paintScreen ();
afterTime = System.nanoTime ();
timeDiff = afterTime - beforeTime;
sleepTime = (period - timeDiff) - overSleepTime;
if (sleepTime > 0)
{
try
{
Thread.sleep (sleepTime / 1000000L);
}
catch (InterruptedException ignored)
{
}
overSleepTime = (System.nanoTime () - afterTime - sleepTime);
}
else
{
excess -= sleepTime;
overSleepTime = 0L;
if (++ noDelays >= NO_DELAYS_PER_YIELD)
{
Thread.yield ();
noDelays = 0;
}
}
beforeTime = System.nanoTime ();
int skips = 0;
while ((excess > period) && (skips < MAX_FRAME_SKIPS))
{
excess -= period;
gameUpdate ();
skips++;
}
}
System.exit (0);
}
private void gameUpdate ()
{
if (! isUserPaused && ! isWindowPaused)
{
updateGame ();
}
}
private void gameRender ()
{
if (dbImage == null)
{
dbImage = createImage (panelWidth, panelHeight);
if (dbImage == null)
{
System.out.println ("Image is null.");
return;
}
else
{
dbg = (Graphics2D) dbImage.getGraphics ();
}
}
dbg.setColor (backgroundColor);
dbg.fillRect (0, 0, panelWidth, panelHeight);
renderGame (dbg);
}
private void paintScreen ()
{
Graphics2D g;
try
{
g = (Graphics2D) this.getGraphics ();
if ((g != null) && (dbImage != null))
{
g.drawImage (dbImage, 0, 0, null);
}
Toolkit.getDefaultToolkit ().sync ();
if (g != null)
{
g.dispose ();
}
}
catch (Exception e)
{
System.out.println ("Graphics context error : " + e);
}
}
public void setWindowPaused (boolean isPaused)
{
isWindowPaused = isPaused;
}
public void setUserPaused (boolean isPaused)
{
isUserPaused = isPaused;
}
}
This again is an abstract class. Its abstract for reusability purpose. You need not know the exact implementation of my game loop. You can create your own custom game panel that inherits from it, and implement the abstract methods of it, and everything will be good to go.
Lets create a Box now:
import java.awt.*;
public class Box extends Rectangle
{
private Color color;
private Direction currentDirection = Direction.None;
private int speed;
public Box (int size, int speed, Color color)
{
super (size, size);
this.speed = speed;
this.color = color;
}
public void update ()
{
switch (currentDirection)
{
case None:
break;
case North:
y -= speed;
break;
case South:
y += speed;
break;
case East:
x += speed;
break;
case West:
x -= speed;
break;
}
}
public void draw (Graphics2D graphics)
{
graphics.setColor (color);
graphics.fill (this);
}
public void setDirection (Direction direction)
{
currentDirection = direction;
}
}
Nothing out of the ordinary here. Its a Rectangle shape, which has update method to update its state based on the Direction it has and a draw method that renders it on screen using the graphics object as context.
The Direction enum used in Box looks as follows:
public enum Direction
{
None,
North,
South,
East,
West
}
So now its time to create our own custom BoxPanel that will inherit from GamePanel. Here's how it looks like:
import java.awt.*;
import java.awt.event.KeyEvent;
public class BoxPanel extends GamePanel
{
private Box box;
public BoxPanel ()
{
super (800, 600, 60, Color.lightGray);
box = new Box (80, 5, Color.darkGray);
}
#Override
protected void consumeKeyPressed (int keyCode)
{
switch (keyCode)
{
case KeyEvent.VK_W:
box.setDirection (Direction.North);
break;
case KeyEvent.VK_S:
box.setDirection (Direction.South);
break;
case KeyEvent.VK_D:
box.setDirection (Direction.East);
break;
case KeyEvent.VK_A:
box.setDirection (Direction.West);
break;
case KeyEvent.VK_SPACE:
box.setDirection (Direction.None);
break;
}
}
#Override
protected void renderGame (Graphics2D graphics)
{
box.draw (graphics);
}
#Override
protected void updateGame ()
{
box.update ();
}
}
It basically creates a box and implements the abstract methods of the GamePanel where all it does is update the box and render the box using appropriate methods present.
The consumeKeyPressed is all about handling key presses, where all I do is set the direction of the box appropriately.
And so finally comes my BoxFrame which wraps everything together into a runnable demonstration:
public class BoxFrame extends GameFrame
{
public BoxFrame ()
{
super ("Box Demo", new BoxPanel ());
}
public static void main (String[] args)
{
new BoxFrame ();
}
}
That's it!
You can use GameFrame and GamePanel in your own projects too. Abstraction really pays off. Doesn't it?
You need not worry if you don't understand part of the game loop or anything. By repeated reading of the code, you'll eventually understand it.
This is a runnable demonstration that shows how you can create smooth movements. I'd suggest you also look into interpolation for creating smooth movements.
You can tweak the FPS value while creating the BoxPanel to vary the smooth factor of the game.
Run the code, read it, re-read it, understand it. Then write it yourself as practice.
FYI, my game loop uses a concept called Double Buffering for rendering the objects smoothly on screen.
You can create other objects too that have update and draw method, and can put their calls in the updateGame and renderGame method of your custom panel, and those objects will appropriately be rendered as well.
The applet plays an animated gif file:
public class Applet1 extends Applet {
private Image m_image=null;
public void init() {
m_image=getImage(getDocumentBase(), "001.gif");
}
public void paint(Graphics g) {
g.drawImage(m_image,0,0,this);
}
public boolean imageUpdate( Image img, int flags, int x, int y, int w, int h ) {
System.out.println("Image update: flags="+flags+" x="+x+" y="+y+" w="+w+" h="+h);
repaint();
return true;
}
}
I need to add the updated image in another program:
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
public class HighlightExample {
public static void main(String[] args) {
JFrame f = new JFrame("Highlight example");
final JTextPane textPane = new JTextPane();
JPanel pane = new JPanel();
pane.setLayout(new BorderLayout());
final JTextField tf = new JTextField();
pane.add(tf, "Center");
f.getContentPane().add(pane, "South");
f.getContentPane().add(new JScrollPane(textPane), "Center");
textPane.setText("abсdefghijkl lmnop12345678");
final WordSearcher searcher = new WordSearcher(textPane);
tf.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
word = tf.getText().trim();
int offset = searcher.search(word);
if (offset != -1) {
try {
textPane.scrollRectToVisible(textPane.modelToView(offset));
} catch (BadLocationException e) {}
}
}
});
textPane.getDocument().addDocumentListener(new DocumentListener() {
public void insertUpdate(DocumentEvent evt) {
searcher.search(word);
}
public void removeUpdate(DocumentEvent evt) {
searcher.search(word);
}
public void changedUpdate(DocumentEvent evt) {
}
});
f.setSize(400, 400);
f.setVisible(true);
}
public static String word;
}
class WordSearcher {
protected JTextComponent comp;
protected Highlighter.HighlightPainter painter;
public WordSearcher(JTextComponent comp) {
this.comp = comp;
this.painter = new UnderlineHighlighter.UnderlineHighlightPainter(Color.red);
}
public int search(String word) {
int firstOffset = -1;
Highlighter highlighter = comp.getHighlighter();
Highlighter.Highlight[] highlights = highlighter.getHighlights();
for (int i = 0; i < highlights.length; i++) {
Highlighter.Highlight h = highlights[i];
if (h.getPainter() instanceof UnderlineHighlighter.UnderlineHighlightPainter) {
highlighter.removeHighlight(h);
}
}
if (word == null || word.equals("")) {
return -1;
}
String content;
try {
Document d = comp.getDocument();
content = d.getText(0, d.getLength()).toLowerCase();
} catch (BadLocationException e) {
return -1;
}
word = word.toLowerCase();
int lastIndex = 0;
int wordSize = word.length();
while ((lastIndex = content.indexOf(word, lastIndex)) != -1) {
int endIndex = lastIndex + wordSize;
try {
highlighter.addHighlight(lastIndex, endIndex, painter);
} catch (BadLocationException e) {}
if (firstOffset == -1) {
firstOffset = lastIndex;
}
lastIndex = endIndex;
}
return firstOffset;
}
}
class UnderlineHighlighter extends DefaultHighlighter{
protected static final Highlighter.HighlightPainter sharedPainter = new UnderlineHighlightPainter(null);
protected Highlighter.HighlightPainter painter;
public static class UnderlineHighlightPainter extends LayeredHighlighter.LayerPainter {
protected Color color; // The color for the underline
public UnderlineHighlightPainter(Color c) {
color = c;
}
public void paint(final Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c) {
}
public Shape paintLayer(final Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c, View view) {
g.setColor(color == null ? c.getSelectionColor() : color);
Rectangle alloc;
try {
Shape shape = view.modelToView(offs0,Position.Bias.Forward, offs1,Position.Bias.Backward, bounds);
alloc = (shape instanceof Rectangle) ? (Rectangle) shape : shape.getBounds();
} catch (BadLocationException e) {
return null;
}
FontMetrics fm = c.getFontMetrics(c.getFont());
int baseline = alloc.y + alloc.height - fm.getDescent() + 1;
g.drawLine(alloc.x, baseline, alloc.x + alloc.width, baseline);
Toolkit kit=Toolkit.getDefaultToolkit();
Image im3 =kit.getImage("001.gif");
g.drawImage(im3,alloc.x+15,alloc.y-6,null);
return alloc;
}
}
}
In this program always displays only the first frame of the image im3. Sorry for a lot of code, it is fully compillable.
It seems like the problem is that you are passing null as the ImageObserver when drawing the image (near the end of your code):
g.drawImage(im3,alloc.x+15,alloc.y-6,null);
So, the image will fire an update when a new frame is ready, but that update just gets ignored because there are no listeners. You probably want to pass c as the ImageObserver so that it will repaint itself when the image changes.
Edit:
This approach will c entirely. If you wanted to be clever, you could create your own ImageObserver that remembers the exact rectangle in c that the icon was painted in, and repaints just that rectangle when notified of an update.