I'm trying to make a simple 2D game in Java.
So far I have a JFrame, with a menubar, and a class which extends JPanel and overrides it's paint method. Now, I need to get a game loop going, where I will update the position of images and so on. However, I'm stuck at how best to achieve this. Should I use multi-threading, because surely, if you put an infinite loop on the main thread, the UI (and thus my menu bar) will freeze up?
Here's my code so far:
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;
#SuppressWarnings("serial")
public class GameCanvas extends JPanel {
public void paint(Graphics g) {
while (true) {
g.setColor(Color.DARK_GRAY);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
#SuppressWarnings("serial")
public class Main extends JFrame {
GameCanvas canvas = new GameCanvas();
final int FRAME_HEIGHT = 400;
final int FRAME_WIDTH = 400;
public static void main(String args[]) {
new Main();
}
public Main() {
super("Game");
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("File");
JMenuItem startMenuItem = new JMenuItem("Pause");
menuBar.add(fileMenu);
fileMenu.add(startMenuItem);
super.add(canvas);
super.setVisible(true);
super.setSize(FRAME_WIDTH, FRAME_WIDTH);
super.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
super.setJMenuBar(menuBar);
}
}
Any pointers or tips? Where should I put my loop? In my main class, or my GameCanvas class?
Your game loop (model) should not be anywhere near any of your GUI classes (view). It uses your GUI classes--but even that you probably want to do through an intermediary (controller). A good way to ensure that you are doing it right is to check that your model doesn't have a single "include javax.swing.???".
The best thing you could do is to keep the game loop in it's own thread. Whenever you want to make a change in the GUI, use SwingWorker or whatever the young kids use now to force it onto the GUI thread for just that one operation.
This is actually awesome because it makes you think in terms of GUI Operations (which would constitute your controller). For instance, you might have a class called "Move" that would have the GUI logic behind a move. Your game loop might instantiate a "Move" with the right values (item to move, final location) and pass it to a GUI loop for processing.
Once you get to that point, you realize that simply adding a trivial "undo" for each GUI operation allows you to easily undo any number of operations. You will also find it easier to replace your Swing GUI with a web GUI...
You need one thread for you game loop and one thread to handle Swing UI events like mouse clicks and keypresses.
When you use the Swing API, you automatically get an additional thread for your UI called the event dispatch thread. Your callbacks are executed on this thread, not the main thread.
Your main thread is fine for your game loop if you want the game to start automatically when the programs runs. If you want to start and stop the game with a Swing GUI, then have then main thread start a GUI, then the GUI can create a new thread for the game loop when the user wants to start the game.
No, your menu bar will not freeze up if you put your game loop in the main thread. Your menu bar will freeze up if your Swing callbacks take a long time to finish.
Data that is shared between the threads will need to be protected with locks.
I suggest you factor your Swing code into its own class and only put your game loop inside your main class. If you're using the main thread for your game loop, this is a rough idea of how you could design it.
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
class GUI extends JFrame {
GameCanvas canvas = new GameCanvas();
final int FRAME_HEIGHT = 400;
final int FRAME_WIDTH = 400;
public GUI() {
// build and display your GUI
super("Game");
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("File");
JMenuItem startMenuItem = new JMenuItem("Pause");
menuBar.add(fileMenu);
fileMenu.add(startMenuItem);
super.add(canvas);
super.setVisible(true);
super.setSize(FRAME_WIDTH, FRAME_WIDTH);
super.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
super.setJMenuBar(menuBar);
}
}
public class Main {
public static void main(String args[]) {
GUI ui = new GUI(); // create and display GUI
gameLoop(); // start the game loop
}
static void gameLoop() {
// game loop
}
}
Java is really suited for event-driven programming. Basically, set up a timer event and listen. At each 'tick-tock' you update your game logic. At each GUI-event you update your data structures that the game logic method will read.
Related
I am making a bot for a game that I play, and have made a GUI so the user of the bot has some control over when the bot is active or not. To do this, I made a start/stop button that would set a variable to true. Each attack is a different class, and they extend the TimerTask class. All they have as an implementation of the run() method that TimerTask has, and they just do
Robot robot = new Robot();
robot.keyPress(KeyEvent.VK_1); //the ".VK_1" part will be different per class
robot.keyRelease(KeyEvent.VK_1);
Ideally, the user would press the button, and TimerTasks would run, simulating keypresses that would be attacks in the game. However, the text on the button does not change, and the TimerTasks never run. Does anyone have a solution for this? My code is below. Thanks in advance!
Main-Class:
package AQWGrindBot;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Timer;
public class Main {
private static boolean playing = false;
static Timer timer = new Timer();
public static void main(String[] args) {
int speed = 1000;
final boolean[] startVar = {false};
JFrame frame = new JFrame("AQW Attack Bot");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400,100);
frame.setResizable(false);
frame.setExtendedState(Frame.MAXIMIZED_BOTH);
JButton start = new JButton("START");
start.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (start.isSelected()) {
playing = !playing;
if (start.getText().equals("START")) {
start.setText("STOP");
toggleBot(true);
} else {
start.setText("START");
toggleBot(false);
}
}
}
});
frame.setLayout(new FlowLayout());
frame.add(start);
frame.setVisible(true);
}
private static void toggleBot(boolean check) {
if (check) {
timer.schedule(new AQWBotATK1(), 0, 1000);
timer.schedule(new AQWBotATK3(), 0, 3000);
timer.schedule(new AQWBotATK4(), 0, 17000);
} else {
timer.cancel();
}
}
}
P.S I am on macOS Mojave. I do not know if this is important, but there are apparently some JFrame features that do not work properly on Mac. Please tell me if I am trying to implement one of them. Also, I am using java.awt and the Robot object to simulate key presses.
You have two main problems, the first is here:
if (start.isSelected()) {
Since start is a JButton, isSelected() will never be true, and if you want this type of functionality, you should be using a JToggleButton or a JCheckBox or JRadioButton (the latter two which extend from JToggleButton) and which change the selected state on press.
So,
JToggleButton start = new JToggleButton("START");
Another option is to create your own boolean field to toggle and to test on button press, but then you don't get a visual feed back from the button as to its state. You could also I suppose use the state of the button's text, obtained via getText()
Your other problem is your use of a java.util.Timer and java.util.TimerTask as this creates code that is potentially not Swing thread-safe. It's almost always better to use javax.swing.Timer or a "Swing Timer" when creating timers for Swing GUI's since the code within the timer's ActionListener is guaranteed to be called on the Swing event thread.
I'm trying to build a simple game in Java. Ran into the problem of the JTextPanel not updating until after the game loop terminates, which of course, isn't a good experience for the player.
I'm unfamiliar with multithreading but trying to figure it out. I can run separate code now in multiple threads, but I can't get my head around how to have the Threads interact. It's highly likely I'm missing something simple, but I can't find it by searching, so I throw myself at your mercy. I'm hanging by a thread...
Controller class and main thread. I need the gamePanel and the game to run separately. I've tried to run the Game class in a separate thread, but the game code isn't running in the gamePanel.
Controller:
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class Controller_LetterFall{
public static void main(String[] args){
SwingUtilities.invokeLater(new Runnable(){
public void run(){
new MainFrame();
}
});
}
}
And the MainFrame Class. I try to run gameplay() in a new thread.
package wordFall;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTextArea;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class MainFrame extends JFrame implements Runnable {
private GamePlay game;
private TextPanel gamePanel;
private Header header;
private Player player;
private Dictionary dictionary;
private GamePlay game;
public MainFrame(){
super("Game");
// Set the size of the frame.
setSize(400,600);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
// Establish default player.
player = new Player();
dictionary = new Dictionary();
game = new GamePlay();
header = new Header();
gamePanel = new TextPanel();
add(header, BorderLayout.NORTH);
add(gamePanel, BorderLayout.CENTER);
this.game.setBoardInterface(
new BoardInterface(){
#Override
public void redraw(String text) {
gamePanel.appendText(text);
}
});
}
#Override
public void run() {
game.play();
System.out.println("The game is over.");
}
}
Any help would be appreciated.
You need something like a thread safe data container. Lets call it GameData.
Your GamePlay object needs to know that GameData object and the UI needs to know that as well because changes made by the user has to be propagated to the GameData object.
In your GamePlay object you can look for changes of the GameData object every second or so. If there are changes than you have something to do if not ..
But it would be better use an event like approach i.e. the Observer Pattern. If changes are made by the UI the GamePlay will be notified by the observable GameData object.
Furthermore, the UI can be notified as well when data changes.
This seperates the concerns very clearly and follows the Model View Controller pattern.
Well, in this case, your MainFrame object is being constructed from the swing dispatcher thread. As you said, you have to start a new thread somewhere to handle the game. Assuming you would have this code in an instance method of a MainFrame object, that would be:
Thread thr = new Thread(this);
thr.start();
Then, whenever your separate game thread wants to update the ui object, it should not do it directly, but use SwingUtilities.invokeLater:
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
/* Update the ui element... */
}});
That runnable will be executed in a swing dispatcher thread when it will have time.
I use .show() before a "blocking" code like a while loop. But even though the .show gets called, the UI doesn't actually show the called panel.
Here is the code that shows the issue:
(WARNING: This code contains a while true loop.)
import javax.swing.JFrame;
import java.awt.CardLayout;
import java.awt.event.ActionEvent;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JButton;
public class CardTest extends JFrame{
public CardTest() {
CardLayout cl = new CardLayout(0,0);
getContentPane().setLayout(cl);
JPanel panelA = new JPanel();
getContentPane().add(panelA, "PanelA");
JLabel lblPanelA = new JLabel("Panel A");
panelA.add(lblPanelA);
JButton btnSwitchToPanel = new JButton("Switch to Panel B");
panelA.add(btnSwitchToPanel);
JPanel panelB = new JPanel();
getContentPane().add(panelB, "PanelB");
JLabel lblPanelB = new JLabel("Panel B");
panelB.add(lblPanelB);
btnSwitchToPanel.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(ActionEvent event) {
cl.show(getContentPane(), "PanelB");
getContentPane().revalidate();
// Here is the problem. Even though cl.show is called first,
// it still doesn't show, before the while loop has terminated.
int i = 0;
while(i < 1000000){
i++;
System.out.println(i);
}
}
});
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setVisible(true);
}
public static void main(String[] args){
new CardTest();
}
}
If you are wondering, I need this for a downloader, where the while true loop (to download a file) is called after pressing a button in the first panel. The second panel contains the progress bar. But the progress panel never gets displayed even though the .show function is called before the download code.
UPDATE
I do know that putting the loop into a new thread, solves the draw problem, but it also introduces other problems, because I rely on sequential execution of functions after the loop(download file(loop), Unzipp file, move those files...).
The best solution would be to find a way to allow the .show() call to actually take the time to switch panes before continuing with the loop.
I use .show() before a "blocking" code like a while loop. But even though the .show gets called, the UI doesn't actually show the called panel.
Yes, because you are "blocking" the Event Dispatch Thread (EDT) which is responsible for repainting the GUI. So the GUI can't repaint itself until the code finishes executing.
You need to create a separate Thread to executing the long running task so you don't block the EDT. One way to do this is to use a SwingWorker. The SwingWorker will create the Thread for you and will notify you when the task is complete so you can update the GUI.
Read the section from the Swing tutorial on Concurrency for more information and a working example.
This happens because you are doing work on the EventDispatchingThread. This Thread is also responsible for actually drawing the GUI.
You have no other choice than doing your work in another thread.
E.g.: (Quick + Dirty)
new Thread(){
#Override
public void run() {
while (...) {...}
}
}.start();
This is because redrawing the UI is done in the same thread as event processing and doesn't happen until after the event processing has completed (i.e., all event handling methods have returned).
Best thing to do is move that "blocking" code into a runnable and execute it in a worker thread.
I am trying to develop a JFrame which has two buttons that would let me to call the main method of other classes. The first try was to put it directly into the actionPerformed of each button, this will cause the JFrame of the other class to open but showing only the title of it and not showing any contents of the JPanel additionally freezing the program (can't even press close button, have to go into task manager or eclipse to kill it). The second try was adding a method call in actionPerformed, and adding the method will this time call the main method of other class however the same result (freeze of program).
For testing purposes I have placed the call to main method of other class, straight in this class main method which has proven to me that the frame of other class has successfully appeared, including all its JPanel contents, functionality etc.
I know I could make some kind of infinite loop in my main method to wait until a boolean is set to true, but then I though there must be some less-expensive way to get it working. So here I am asking this question to you guys.
Here is the code of the 2nd try;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class Chat {
public static void main (String[] args) {
JFrame window = new JFrame("Chat Selection");
//Set the default operation when user closes the window (frame)
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//Set the size of the window
window.setSize(600, 400);
//Do not allow resizing of the window
window.setResizable(false);
//Set the position of the window to be in middle of the screen when program is started
window.setLocationRelativeTo(null);
//Call the setUpWindow method for setting up all the components needed in the window
window = setUpWindow(window);
//Set the window to be visible
window.setVisible(true);
}
private static JFrame setUpWindow(JFrame window) {
//Create an instance of the JPanel object
JPanel panel = new JPanel();
//Set the panel's layout manager to null
panel.setLayout(null);
//Set the bounds of the window
panel.setBounds(0, 0, 600, 400);
JButton client = new JButton("Run Client");
JButton server = new JButton("Run Server");
JLabel author = new JLabel("By xxx");
client.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
//run client main
runClient();
}
});
server.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
//run server main
}
});
panel.add(client);
client.setBounds(10,20,250,200);
panel.add(server);
server.setBounds(270,20,250,200);
panel.add(author);
author.setBounds(230, 350, 200, 25);
window.add(panel);
return window;
}
private static void runClient() {
String[] args1={"10"};
ClientMain.main(args1);
}
}
Only one main method is allowed per application. Honestly I am not sure what you are trying to do or think is supposed to happen when you call main on other classes. When you call main on other classes all you are doing is calling a method that happens to be called main and passing args to it. Your freezing is probably because you are not using Swing correctly:
http://docs.oracle.com/javase/tutorial/uiswing/concurrency/initial.html
The problem you're having is that Java Swing is single threaded. When you're running the main function of the other class, however you do it, the GUI won't be able to keep running until it returns. Try spawning off a new thread that calls the second main method.
private static void runClient() {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
String[] args1={"10"};
ClientMain.main(args1);
}
});
}
EDIT: Updated, as per #Radiodef's suggestion. Missed at the top when you said this second class had to display things on the GUI. Definitely want to go with the invokeLater then.
I'm trying to make an application that runs an animation. To do this I've got a Jframe that contains my subclass of Jpanel in which the animation runs. Here are my two classes:
Firstly, here's my driver class:
import javax.swing.*;
public class Life {
public static void main(String[] args){
JFrame game = new JFrame("Life");
game.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
game.setSize(500, 500);
MainPanel mainPanel = new MainPanel();
game.setContentPane(mainPanel);
game.setVisible(true);
}
}
Secondly, here's my subclass of Jpanel:
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class MainPanel extends JPanel implements ActionListener{
int i = 0;
int j = 0;
public MainPanel(){
super();
}
public void paintComponent(Graphics g){
j++;
g.drawLine(10,10, 20 + i, 20 + i);
Timer t = new Timer(1000, this);
t.start();
}
#Override
public void actionPerformed(ActionEvent actionEvent) {
i++;
repaint();
}
}
Notice that the variable i is incremented each time actionPreformed is called and the variable j is called each time paintComponent is called. What winds up happening is that i starts to be much larger than j and the line drawn by paintComponent seems to grow at a faster and faster rate.
Here are my questions:
Why does this happen?
How can I sync things up so that the line gets redrawn each every 1000 ms?
Given what I'm trying to do, is my approach wrong? Should I be doing things differently?
Thanks in advance.
Don't start a Swing Timer from within a paintComponent method.
This method should do your painting, only your painting and nothing but painting. It should contain absolutely no program logic. Understand that you have very limited control over this method since you cannot predict when or if it will be called, how often it will be called. You can't even call it yourself or be guaranteed that when you suggest it be called via repaint() that it will in fact be called.
Also this method must be fast, as fast possible since anything that slows it down, be it object creation or reading in files will reduce the perceived responsiveness of your GUI, the last thing that you want to see happen.
The solution is to separate the program logic out of that method and into better methods such as your constructor. Repeating code should be in a Swing Timer.
Edit:
You state:
I just did that to test things out. One more question: What happens if paintComponent, or some thread on which the work in paintComponent depends, takes longer than 1000 ms (or whatever it is) to do its work? The only thing I can think of is having paintComponent paint the progress of the animation so far, rather than waiting for the animation to reach the next step(if that makes any sense). Thoughts?
You should never have code in paintComponent that takes that long or even 10's of milliseconds. If there's a risk of something like that happening, then do the drawing in a background thread and to a BufferedImage, and then on the Swing event thread show the BufferedImage in paintComponent method using the Graphics#drawImage(...) method.
A few minor additions to #HFoE's essential insights:
A public start() method is a handy way to ensure that your view is completely constructed before starting.
Fields have well-defined default values, and they should be private.
Swing GUI objects should be constructed and manipulated only on the event dispatch thread.
Override getPreferredSize() and pack() the enclosing Window.
Revised code:
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
public class Life {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
private final JTabbedPane jtp = new JTabbedPane();
#Override
public void run() {
JFrame game = new JFrame("Life");
game.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
MainPanel mainPanel = new MainPanel();
game.setContentPane(mainPanel);
game.pack();
game.setVisible(true);
mainPanel.start();
}
});
}
private static class MainPanel extends JPanel implements ActionListener {
private Timer t = new Timer(100, this);
private int i;
private int j;
#Override
public void paintComponent(Graphics g) {
g.drawLine(10, 10, 20 + i, 20 + i);
}
public void start() {
t.start();
}
#Override
public void actionPerformed(ActionEvent actionEvent) {
i++;
repaint();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(500, 500);
}
}
}
A Timer by defaults keep on running. Only when you call setRepeats( false ) it will stop.
So the following lines
Timer t = new Timer(1000, this);
t.start();
in your paintComponent method means that after a few repaints you will have a number of Timer instances running, explaining why i increases that much faster then j.
The solution is of course to move your Timer outside the paintComponent method, and stick to one Timer instance.
Further remarks (which weren't said by the others, not gonna repeat their very useful advise):
Never override the paintComponent method without calling the super method
You shouldn't expose the ActionListener interface. Just use an ActionListener internally