I am currently building a RPG game using built-in GUI and is trying to display a simple fight scene on a JDialogue. My partial code are as follows:
public void setupFight(Character c) {
fightPopup.setLocationRelativeTo(this);
fightPopup.setVisible(true);
mcBar.setMinimum(0);
mcBar.setMaximum(mc.getAtkspd());
enemyBar.setMinimum(0);
enemyBar.setMaximum(c.getAtkspd());
enemyDm.setVisible(false);
enemyDmType.setVisible(false);
mcDm.setVisible(false);
mcDm.setVisible(false);
mcFight.setVisible(true);
enemyFight.setVisible(true);
fight(c);
}
public void fight(Character c) {
mc.setAtkbr(mc.getAtkspd());
c.setAtkbr(c.getAtkspd());
int[] damage1 = new int[2];
int[] damage2 = new int[2];
System.out.println(mc.getAtk());
while (mc.getHP() > 0 && c.getHP() > 0) {
try {
Thread.sleep(10);
}
catch (InterruptedException ex) {
Logger.getLogger(Character.class.getName()).log(Level.SEVERE, null, ex);
}
mc.setAtkbr(mc.getAtkbr() - 10);
c.setAtkbr(c.getAtkbr() - 10);
mcBar.setValue(mcBar.getMaximum() - mc.getAtkbr());
enemyBar.setValue(enemyBar.getMaximum() - c.getAtkbr());
enemyDmType.setText("hola");
if (mc.getAtkbr() == 0 && c.getAtkbr() == 0) {
damage1 = mc.attack(c);
damage2 = c.attack(mc);
mc.setHP(mc.getHP() - damage1[1]);
c.setHP(c.getHP() - damage2[1]);
mc.setAtkbr(mc.getAtkspd());
c.setAtkbr(c.getAtkspd());
}
else if (mc.getAtkbr() == 0) {
damage1 = mc.attack(c);
mc.setHP(mc.getHP() - damage1[1]);
mc.setAtkbr(mc.getAtkspd());
}
else if (c.getAtkbr() == 0) {
damage2 = c.attack(mc);
c.setHP(c.getHP() - damage2[1]);
c.setAtkbr(c.getAtkspd());
}
}
if (mc.getHP() <= 0) {
((MC) mc).defeat();
}
else {
((MC) mc).pickUp(((Enemy) c).defeat());
}
}
This is what the Dialogue supposed to look like: enter image description here
But this is what showes up when the code runs and the while loop running (which is basically empty without any components): enter image description here
The components only show up on the JDialogue after the "supposed" fight scene is over (finished simulated by the code, after the line ((MC) mc).pickUp(((Enemy) c).defeat());
) with the final updated data.
Could anyone help me with this please? I really appreciate it.
I have tried to extend the Thread.sleep(10);
time as I thought it was due to 10ms is two short for the computer to update any information on the Dialogue. But it seems like it's pausing everything, not only the while loop itself.
Swing is single threaded, meaning that you shouldn't be perform long running or blocking operations within its context (like doing things like Thread.sleep
).
Instead, you need to decouple the workflow in some way, either running the logic in a seperate thread or utilising a some kind of timed callback (ie a Timer
). Which you would use depends on what you want to achieve.
Please note, the following examples are intended to demonstrate how the solutions might work so that you can take the concept and implement them in your code, not provide a "copy-paste" solution based on your code. These are fully runnable examples, so you can copy and run them to see how they work.
SwingWorker
See Worker Threads and SwingWorker for more details...
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingWorker;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
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("Test");
frame.add(
new FightPane(
new Entity("A", 100),
new Entity("B", 100)
)
);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Entity {
private String name;
private int hitPonts;
private int health;
private Random rnd = new Random();
public Entity(String name, int hitPonts) {
this.name = name;
this.health = hitPonts;
this.hitPonts = hitPonts;
}
public String getName() {
return name;
}
public int getHitPonts() {
return hitPonts;
}
public int getHealth() {
return health;
}
public int attack() {
return rnd.nextInt(0, 25);
}
public void damage(int amount) {
health = Math.max(0, health - amount);
}
public void heal(int amount) {
health = Math.min(hitPonts, health + amount);
}
@Override
public String toString() {
return getName() + " - " + getHealth() + "/" + getHitPonts();
}
}
public class EntityPane extends JPanel {
private JLabel name;
private JLabel health;
private JProgressBar pbHealth;
private Entity entity;
public EntityPane(Entity entity) {
this.entity = entity;
name = new JLabel();
name.setFont(name.getFont().deriveFont(Font.BOLD, 16));
health = new JLabel();
pbHealth = new JProgressBar();
pbHealth.setMinimum(0);
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.LINE_START;
gbc.gridwidth = GridBagConstraints.REMAINDER;
add(name, gbc);
add(health, gbc);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1;
add(pbHealth, gbc);
setBorder(new CompoundBorder(new LineBorder(Color.BLACK, 1, true), new EmptyBorder(8, 8, 8, 8)));
refreshDetails();
}
public void didAttack() {
setBackground(null);
refreshDetails();
}
public void wasAttacked() {
setBackground(Color.ORANGE);
refreshDetails();
}
public void didWin() {
setBackground(Color.GREEN);
refreshDetails();
}
public void didLose() {
setBackground(Color.RED);
refreshDetails();
}
public Entity getEntity() {
return entity;
}
// This would be better handled via an observer directly on Entity
// but for example purposes
public void refreshDetails() {
Entity entity = getEntity();
if (entity == null) {
name.setText("---");
health.setText("---");
pbHealth.setValue(0);
}
name.setText(entity.getName());
health.setText(entity.getHealth() + "/" + entity.getHitPonts());
pbHealth.setMaximum(entity.getHitPonts());
pbHealth.setValue(entity.getHealth());
}
}
protected class FightPane extends JPanel {
private EntityPane fighterOnePane;
private EntityPane fighterTwoPane;
public FightPane(Entity fighter1, Entity fighter2) {
setBorder(new EmptyBorder(8, 8, 8, 8));
setLayout(new GridBagLayout());
JLabel title = new JLabel("Fight!");
title.setFont(title.getFont().deriveFont(Font.BOLD, 32));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(8, 8, 8, 8);
gbc.gridwidth = GridBagConstraints.REMAINDER;
add(title, gbc);
fighterOnePane = new EntityPane(fighter1);
fighterTwoPane = new EntityPane(fighter2);
JPanel fighters = new JPanel(new GridBagLayout());
GridBagConstraints gbcFight = new GridBagConstraints();
gbcFight.gridheight = GridBagConstraints.REMAINDER;
gbcFight.insets = new Insets(8, 8, 8, 8);
fighters.add(fighterOnePane, gbcFight);
fighters.add(new JLabel("vs"), gbcFight);
fighters.add(fighterTwoPane, gbcFight);
add(fighters, gbc);
JButton fightButton = new JButton("Start");
add(fightButton, gbc);
fightButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
fightButton.setEnabled(false);
FightWorker worker = new FightWorker(fighterOnePane.getEntity(), fighterTwoPane.getEntity(), new FightWorker.Obsever() {
@Override
public void update(Entity fighter) {
if (fighter == fighterOnePane.getEntity()) {
fighterOnePane.wasAttacked();
fighterTwoPane.didAttack();
} else if (fighter == fighterTwoPane.getEntity()) {
fighterOnePane.didAttack();
fighterTwoPane.wasAttacked();
}
}
@Override
public void winner(Entity fighter) {
EntityPane target = null;
if (fighter == fighterOnePane.getEntity()) {
fighterOnePane.didWin();
fighterTwoPane.didLose();
title.setText(fighter.getName() + " did win!");
} else if (fighter == fighterTwoPane.getEntity()) {
fighterOnePane.didLose();
fighterTwoPane.didWin();
title.setText(fighter.getName() + " did win!");
} else {
fighterOnePane.didLose();
fighterTwoPane.didLose();
title.setText("No one won!");
}
// Don't tap the button twice, but this shows away
// you could re-enable controls or do things
// when the fight is over
fightButton.setEnabled(true);
}
});
worker.execute();
}
});
}
}
protected class FightWorker extends SwingWorker<Entity, Entity> {
// This provides a simpilified callback workflow and decouples
// the worker from the obsever. The worker focucs only on the
// interaction between the entities and nothing else
public interface Obsever {
public void update(Entity fighter);
public void winner(Entity fighter);
}
private Entity fighter1;
private Entity fighter2;
private Obsever obsever;
public FightWorker(Entity fighter1, Entity fighter2, Obsever obsever) {
this.fighter1 = fighter1;
this.fighter2 = fighter2;
this.obsever = obsever;
// This allows for a self monitoring workflow, so when the
// worker is "done", we can easily notify the obeserver. These
// notifications occur on the EDT
addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (isDone()) {
try {
Entity winner = get();
obsever.winner(winner);
} catch (InterruptedException | ExecutionException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
obsever.winner(null);
}
}
}
});
}
@Override
protected void process(List<Entity> chunks) {
// This is called within the EDT, so we can update the UI
for (Entity entity : chunks) {
obsever.update(entity);
}
}
@Override
protected Entity doInBackground() throws Exception {
Random rnd = new Random();
while (fighter1.getHealth() > 0 && fighter2.getHealth() > 0) {
// Randomly choose who gets to attack
if (rnd.nextBoolean()) {
fighter2.damage(fighter1.attack());
publish(fighter2);
} else {
fighter1.damage(fighter2.attack());
publish(fighter1);
}
Thread.sleep(500);
}
Entity winner = null;
if (fighter1.getHealth() > 0 && fighter2.getHealth() == 0) {
winner = fighter1;
} else if (fighter1.getHealth() == 0 && fighter2.getHealth() > 0) {
winner = fighter2;
}
return winner;
}
}
}
Timer
A Timer
acts as a pseudo loop with a delay. So each time it "ticks" it represents an iteration of the loop.
A Swing Timer
ticks on the Event Dispatching Thread, making it possible to update the UI directly from.
See How to Use Swing Timers for more details.
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.Timer;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import stackoverflow.Main.FighterEngine.Obsever;
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("Test");
frame.add(
new FightPane(
new Entity("A", 100),
new Entity("B", 100)
)
);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Entity {
private String name;
private int hitPonts;
private int health;
private Random rnd = new Random();
public Entity(String name, int hitPonts) {
this.name = name;
this.health = hitPonts;
this.hitPonts = hitPonts;
}
public String getName() {
return name;
}
public int getHitPonts() {
return hitPonts;
}
public int getHealth() {
return health;
}
public int attack() {
return rnd.nextInt(0, 25);
}
public void damage(int amount) {
health = Math.max(0, health - amount);
}
public void heal(int amount) {
health = Math.min(hitPonts, health + amount);
}
@Override
public String toString() {
return getName() + " - " + getHealth() + "/" + getHitPonts();
}
}
public class EntityPane extends JPanel {
private JLabel name;
private JLabel health;
private JProgressBar pbHealth;
private Entity entity;
public EntityPane(Entity entity) {
this.entity = entity;
name = new JLabel();
name.setFont(name.getFont().deriveFont(Font.BOLD, 16));
health = new JLabel();
pbHealth = new JProgressBar();
pbHealth.setMinimum(0);
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.LINE_START;
gbc.gridwidth = GridBagConstraints.REMAINDER;
add(name, gbc);
add(health, gbc);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1;
add(pbHealth, gbc);
setBorder(new CompoundBorder(new LineBorder(Color.BLACK, 1, true), new EmptyBorder(8, 8, 8, 8)));
refreshDetails();
}
public void didAttack() {
setBackground(null);
refreshDetails();
}
public void wasAttacked() {
setBackground(Color.ORANGE);
refreshDetails();
}
public void didWin() {
setBackground(Color.GREEN);
refreshDetails();
}
public void didLose() {
setBackground(Color.RED);
refreshDetails();
}
public Entity getEntity() {
return entity;
}
// This would be better handled via an observer directly on Entity
// but for example purposes
public void refreshDetails() {
Entity entity = getEntity();
if (entity == null) {
name.setText("---");
health.setText("---");
pbHealth.setValue(0);
}
name.setText(entity.getName());
health.setText(entity.getHealth() + "/" + entity.getHitPonts());
pbHealth.setMaximum(entity.getHitPonts());
pbHealth.setValue(entity.getHealth());
}
}
protected class FightPane extends JPanel {
private EntityPane fighterOnePane;
private EntityPane fighterTwoPane;
public FightPane(Entity fighter1, Entity fighter2) {
setBorder(new EmptyBorder(8, 8, 8, 8));
setLayout(new GridBagLayout());
JLabel title = new JLabel("Fight!");
title.setFont(title.getFont().deriveFont(Font.BOLD, 32));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(8, 8, 8, 8);
gbc.gridwidth = GridBagConstraints.REMAINDER;
add(title, gbc);
fighterOnePane = new EntityPane(fighter1);
fighterTwoPane = new EntityPane(fighter2);
JPanel fighters = new JPanel(new GridBagLayout());
GridBagConstraints gbcFight = new GridBagConstraints();
gbcFight.gridheight = GridBagConstraints.REMAINDER;
gbcFight.insets = new Insets(8, 8, 8, 8);
fighters.add(fighterOnePane, gbcFight);
fighters.add(new JLabel("vs"), gbcFight);
fighters.add(fighterTwoPane, gbcFight);
add(fighters, gbc);
JButton fightButton = new JButton("Start");
add(fightButton, gbc);
fightButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
fightButton.setEnabled(false);
FighterEngine worker = new FighterEngine(fighterOnePane.getEntity(), fighterTwoPane.getEntity(), new FighterEngine.Obsever() {
@Override
public void update(Entity fighter) {
if (fighter == fighterOnePane.getEntity()) {
fighterOnePane.wasAttacked();
fighterTwoPane.didAttack();
} else if (fighter == fighterTwoPane.getEntity()) {
fighterOnePane.didAttack();
fighterTwoPane.wasAttacked();
}
}
@Override
public void winner(Entity fighter) {
EntityPane target = null;
if (fighter == fighterOnePane.getEntity()) {
fighterOnePane.didWin();
fighterTwoPane.didLose();
title.setText(fighter.getName() + " did win!");
} else if (fighter == fighterTwoPane.getEntity()) {
fighterOnePane.didLose();
fighterTwoPane.didWin();
title.setText(fighter.getName() + " did win!");
} else {
fighterOnePane.didLose();
fighterTwoPane.didLose();
title.setText("No one won!");
}
// Don't tap the button twice, but this shows away
// you could re-enable controls or do things
// when the fight is over
fightButton.setEnabled(true);
}
});
worker.execute();
}
});
}
}
protected class FighterEngine {
// This provides a simpilified callback workflow and decouples
// the worker from the obsever. The worker focucs only on the
// interaction between the entities and nothing else
public interface Obsever {
public void update(Entity fighter);
public void winner(Entity fighter);
}
private Entity fighter1;
private Entity fighter2;
private Obsever obsever;
private Timer timer;
public FighterEngine(Entity fighter1, Entity fighter2, Obsever obsever) {
this.fighter1 = fighter1;
this.fighter2 = fighter2;
this.obsever = obsever;
timer = new Timer(500, new ActionListener() {
private Random rnd = new Random();
@Override
public void actionPerformed(ActionEvent e) {
if (fighter1.getHealth() == 0 || fighter2.getHealth() == 0) {
Entity winner = null;
if (fighter1.getHealth() > 0 && fighter2.getHealth() == 0) {
winner = fighter1;
} else if (fighter1.getHealth() == 0 && fighter2.getHealth() > 0) {
winner = fighter2;
}
obsever.winner(winner);
timer.stop();
}
if (rnd.nextBoolean()) {
fighter2.damage(fighter1.attack());
obsever.update(fighter2);
} else {
fighter1.damage(fighter2.attack());
obsever.update(fighter1);
}
}
});
timer.setInitialDelay(0);
}
public void execute() {
if (timer.isRunning()) {
return;
}
timer.start();
}
}
}
The two examples only differ in the FighterWork
and FighterEngine
classes, they are otherwise identical - this is a nice demonstration of decoupling concerns and dependency injection.