Reputation: 593
I need some help with threads in java.
I'm currently working on a project, what compiles a class at runtime and invokes it's main method. The class represents a guy in a territory, which is visible as a canvas to the user. This main method invokes some other methods. Either methods the user typed into an editor or predefined methods from a super class. The editor content could look like this:
main() {
System.out.println("test users class main");
takeAll();
takeAll();
}
public void takeAll() {
for (int i = 0; i < 2; i++)
move();
takeHoney();
takeHoney();
takeHoney();
for (int i = 0; i < 2; i++)
move();
}
The above code is what the user later enters into an editor inside the GUI, which will be compiled when he uses a certain button. He is supposed to learn imperative programming.
The methods main, move and takeHoney are defined in a superclass and takeAll is a method defined by the user at runtime. My program adds the class prefix and compiles the users class.
The user should be able to start, pause, resume, and terminate the main method by clicking some buttons in the GUI.
When I just run the main method all methods finish too fast. The user will only see the result, but not the steps and won't be able to interact while it's runnning.
So far I created a new runnable and started a thread.
protagonistMainMethodRunnable = new Runnable() {
@Override
public void run() {
protagonist.main();
terminateWasPressed();
}
};
//.....
Thread thread = new Thread(protagonistMainMethodRunnable);
thread.run();
"protagonist" is an instance of the users class, which was compiled at runtime.
I'm not very good with threads and can't find an idea to create a delay after each method call.
Does someone have an idea to create a delay after each method call in the main or even after every method call?
EDIT: The idea from James_D was very helpful. Here is the class that works for my use case:
public class OperationQueue {
private Queue<Runnable> operationQueue = new LinkedList<Runnable>();
private Timeline timeline;
public OperationQueue(double delay) {
timeline = null;
setDelay(delay);
}
public void setDelay(double seconds) { //careful: seconds > 0
if (timeline != null)
timeline.stop();
System.out.println("Set keyframe duration to " + seconds + " seconds.");
timeline = new Timeline(new KeyFrame(Duration.seconds(seconds), e -> {
if (!operationQueue.isEmpty()) {
operationQueue.remove().run();
}
}));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
public Queue<Runnable> getOperationQueue() {
return operationQueue;
}
public void add(Runnable queueItem) {
this.operationQueue.add(queueItem);
}
public void clearQueue() {
this.operationQueue.clear();
}
}
I moved the timeline to a setter method. In that way you can change the delay at runtime (with a slider for example).
Upvotes: 1
Views: 2088
Reputation: 3615
I think what you're looking for is what is called setTimeout
in javascript. Unfortunately, Java doesn't have such a method, and you have to rely on Thread.sleep
to emulate it. Again, unfortunately, calling Thread.sleep
on the JavaFX Application Thread will hang the application, so you need to use a JavaFX Timeline
.
What you can do is declare an interface that will automate this for you:
class Delayed {
private static long totalMillis;
private static Timeline timeline = new Timeline();
static void setTimeout(Runnable r, long millis) {
timeline.getKeyFrames().add(new KeyFrame(new Duration(totalMillis += millis), event -> r.run()));
}
static void play() {
timeline.play();
}
}
A method call
System.out.println("Hello World!");
then becomes:
Delayed.setTimeout(() ->System.out.println("Hello World!"), 1000);
Delayed.play();
What I suggest is that you take the code you have (the one that's been entered by the user), and while it's still a String
, that you convert all of the method calls to a Delayed.setTimeout
call, like so:
final int TIMEOUT = 1000;
StringBuilder newCode = new StringBuilder();
Arrays.stream(codeString.split("\n")).forEach(s ->
{
if (s.endsWith(";")) newCode.append("Delayed.setTimeout(() -> ")
.append(s.substring(0, s.length() - 1)) //remove ";"
.append(", ").append(TIMEOUT)
.append(");")
.append("\n");
else {
newCode.append(s + "\n");
}
});
newCode.append("Delayed.play();");
Of course, the s.endsWith(";")
check is very naive, and won't work with things such as if statements, do/while loops, or statements on multiple lines. I couldn't find a more efficient way to check if a line was a complete statement, but this should work fine for very simple code such as the one that seems to be your concern.
EDIT Now working on JavaFX Application Thread.
Upvotes: 0
Reputation: 209225
This is just an outline of how I might approach this, as the question really is too broad.
You are really asking how you can perform an animation that the user defines in code. (It's an animation because you are displaying a collection of frames, where each frame is defined by performing an operation, and there is a time gap between the operations.)
Consider creating a queue of operations to perform:
private class UI {
private Queue<Runnable> operationQueue = new LinkedList<Runnable>();
public Queue<Runnable> getOperationQueue() {
return operationQueue();
}
// ...
}
Now you can run an animation via a Timeline
that periodically checks the queue, and if there's something in it, executes that operation:
public UI () {
// set up ui, etc...
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), e-> {
if (! operationQueue.isEmpty()) {
operationQueue.remove().run();
}
}));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
Now make your predefined basic methods private, and define public methods that submit those private methods to the queue:
public class BaseClass {
private final UI ui = ... ;
private void doMove() {
// implementation here...
}
public void move() {
ui.getOperationQueue().add(this::doMove);
}
private void doTakeHoney() {
// implementation here...
}
public void takeHoney() {
ui.getOperationQueue().add(this::doTakeHoney);
}
}
Note there is actually no threading here at all. Everything is on the FX Application Thread; the timing is controlled by the Timeline
.
Upvotes: 1
Reputation: 1
You can override the superclass's method which you want to run slowly, such as move():
void move() throws Exception {
Thread.sleep(1000);
super.move();
Thread.sleep(1000);
}
If you want to make your code can be controlled by UI button,then you will add some switches.
Upvotes: 0