3

I am writing a java game in javafx, but I think the solution to this question isn't unique to javafx...

I have a Entity class and a bunch of its subclasses such as Missiles, Lasers, etc. However, when the Missiles and Lasers are created by characters in the game, they always keep running until they hit the rectangular boundary of the canvas or when they hit a character and disappear.

However, I expect that there are many other behaviors that the missiles/lasers can have:

  • A missile can keep flying until it disappears 4 seconds later
  • A missile can keep flying until it dies out after traveling for 600 pixels(this is similar to the previous one)
  • A laser can exist in a rectangular zone or move with the character until it dies out after 4 seconds.

The question is, how can we achieve this timed effect? (Maybe propertyChangeListener?) Or should I add stuff to the Entity itself, or should I consider altering my Controller Class? Here are the codes I have:

public abstract class Entity implements Collidable{

private double x;
private double y;
private int z;
private double velocityX;
private double velocityY;
private Image img;
private boolean ally;
protected double width;
protected double height;

    public Entity(int x,int y,int z,boolean b,Image hihi)
    {
     setX(x);
     setY(y);
     setZ(z);
      ally=b;
     setVelocityX(0);
     setVelocityY(0);
     img= hihi;
    }
    public void move()
    {       
        x+=getVelocityX();
        y+=getVelocityY();
    }

  ...
  ...
}

public class Controller {

private List<BattleShip> bs;
private List<Missile> m;
private Rectangle2D rect;


public Controller()
{
    bs= new ArrayList<BattleShip>();
    m= new ArrayList<Missile>();
    rect= new Rectangle2D(-300, -300, 1300, 1050);
}
public void update()
{

    for(int i = bs.size() - 1; i >= 0; i --) {
        bs.get(i).move();
        if (!rect.contains(bs.get(i).getRect())) {
        bs.remove(i);
              }
        }
    for(int i = m.size() - 1; i >= 0; i --) {
        m.get(i).move();
          if (!rect.contains(m.get(i).getRect())) {
            m.remove(i);
          }
        }
    collide();
}

Update[ Looks good : ) ] :

enter image description here

Ptolemorphism
  • 101
  • 2
  • 11
  • 1
    You might check out the [FXGL](https://github.com/AlmasB/FXGL) JavaFX game library, which might have constructs for this in place. – M. le Rutte Oct 15 '17 at 21:15
  • @M.leRutte I will definitely consider your proposal! However, for now I am trying to stick to standard java api because that will also help improve me on parts of java other than game programming... – Ptolemorphism Oct 15 '17 at 21:24
  • @JKostikiadis Your method works very well! Thanks for your help!! – Ptolemorphism Oct 16 '17 at 00:28

2 Answers2

3

In a comment, you asked me how you might use a single thread to manage the timed destruction of multiple entities. First, let's not think about it in terms of using threads. What do we really want to do? We want to perform time-delayed actions.

How might we do this? Well...

Introduction to Event Loop Scheduling

We can create a scheduler to perform actions (or respond to events) at a specific time. These might be one-time actions, or recurring actions that repeat at a fixed interval. You will likely end up with many such actions in your game. How might we implement this? Well, we don't actually have to; it's been done many times, and done quite well. But the jist of it is this:

  1. Create a queue to hold scheduled work items. This should be a priority queue, sorted such that the work items occurring nearest in time appear closest to the front of the queue.
  2. Kick off a processing thread that waits on a queue.
  3. Various game components schedule work items by inserting them into the queue (in a thread-safe manner). Upon insertion, they signal the processing thread to wake up and check the queue.
  4. The processing thread, upon waking up, checks to see if there are work items in the queue. If it finds one, it looks at its completion time and compares it against the current game time.
    • If it's time to run the work item, it does so. Repeat step 4 for the next item in the queue, if there is one.
    • If it's not time to run the work item, the thread puts itself to sleep until dueTime - currentTime has elapsed.
    • Alternatively, if the queue is empty, sleep indefinitely; we'll get woken up when the next work item is scheduled.

This kind of scheduler is known as an event loop: it runs in a "dequeue, run, wait, repeat" loop. A good example can be found in RxJava. You could use it like this:

import io.reactivex.Scheduler;
import io.reactivex.disposables.SerialDisposable;

public final class GameSchedulers {
    private static final Scheduler EVENT_LOOP =
        io.reactivex.schedulers.Schedulers.single();

    public static Scheduler eventLoop() {
        return EVENT_LOOP;
    }
}

public abstract class Entity implements Collidable {
    private final SerialDisposable scheduledDestruction = new SerialDisposable();
    private volatile boolean isDestroyed;

    public void destroyNow() {
        this.isDestroyed = true;
        this.scheduledDestruction.dispose();
    }

    public void destroyAfter(long delay, TimeUnit unit) {
        scheduledDestruction.set(
            GameSchedulers.eventLoop()
                          .scheduleDirect(this::destroyNow, delay, unit)
        );
    }

    /* (rest of class omitted) */
}

To schedule an entity to be destroyed after 4 seconds, you would call entity.destroyAfter(4L, TimeUnit.SECONDS). That call would schedule the destroyNow() method to be called after a 4 second delay. The scheduled action is stored in a SerialDisposable, which can be used to 'dispose' of some object. In this case, we use it to track the scheduled destruction action, and the 'disposal' amounts to a cancellation of that action. In the example above, this serves two purposes:

  1. If the entity gets destroyed by some other means, e.g., the player shoots and destroys a missile, you can simply call destroyNow(), which in turn cancels any previously scheduled destruction (which would now be redundant).
  2. If you want to change the destruction time, you can just call destroyAfter a second time, and if the originally scheduled action hasn't occurred yet, it will be canceled and prevented from running.

Caveats

Games are an interesting case, specifically with regards to time. Consider:

  1. Time in a game does not necessarily proceed at a constant rate. When a game experiences poor performance, the flow of time generally slows accordingly. It's also (usually) possible to pause time.

  2. A game may rely on more than one 'clock'. The player may pause gameplay, effectively freezing the 'game time' clock. Meanwhile, the player may still interact with game menus and options screens, which might be animated according to 'real' time (e.g. system time).

  3. Game time usually flows in one direction, while system time does not. Most PCs these days keep their system clock synchronized with a time server, so the system time is constantly being corrected for 'drift'. Thus, it is not unusual for the system clock to jump backward in time.

  4. Because system time tends to fluctuate slightly, it's not smooth. However, we're at the system scheduler's mercy when it comes to running our code. If we set a goal of advancing game time by one 'tick' 60 times a second (to target 60fps), we need to understand that our 'ticks' will almost never happening exactly when we want them to. Thus, we ought to interpolate: if our tick occurred slightly before or after we expected it, we should advance our game time by slightly less or more than one 'tick'.

These kinds of considerations may prevent you from using a third-party scheduler. You might still use one early in development, but eventually you'll need one that proceeds according to game time and not system time. RxJava actually has a scheduler implementation called TestScheduler that is controlled by an external clock. However, it is not thread safe, and relies on an external actor manually advancing time, but you could use it as a model for your own scheduler.

Mike Strobel
  • 25,075
  • 57
  • 69
  • I don't usually comment on good answers, I usually just vote, but, this is a great answer :-) – jewelsea Oct 17 '17 at 22:03
  • I am a little busy these days, so I haven't tried your suggestions yet. However, as my game progresses, I am sure that your advice will be very helpful to me. Thank you very much! I will let you know if I make big progresses – Ptolemorphism Oct 21 '17 at 01:20
2

Well, I am not expert in the game industry but this is what I suggest :

  1. Have a variable ( boolean) showing the state of the object.
    • private volatile boolean isDestroyed = false;

Note: the volatile is necessary!

  1. If the game object has a time limit ( self-destruct ) then upon creation inside its constructor ( or generally inside its class ) start a Task sleeping for the duration you want and at the end of the task update the isDestroyed variable to true.

Note: You can also put an animation for the destruction of the game object inside the Task.

  1. In the main update() where you are doing all the rendering operations, you can either ignore rendering the game object or remove it from the list ( Be careful to avoid ConcurrentModificationException )

Edit : So let's try an example.. the code below its not the optimal way of drawing/moving shapes it's just to show my solution.

Main class

import java.util.ArrayList;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.stage.Stage;

public class TestApp extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {

        Group root = new Group();
        Scene theScene = new Scene(root);
        stage.setScene(theScene);

        Canvas canvas = new Canvas(512, 820);
        root.getChildren().add(canvas);

        GraphicsContext gc = canvas.getGraphicsContext2D();

        ArrayList<Lasser> allLassers = new ArrayList<>();

        Random randGen = new Random();

        for (int i = 0; i < 10; i++) {
            // create 10 lessers with different self-destruction time
            // on random places
            allLassers.add(new Lasser(randGen.nextInt(500) + 10, 800, i * 1000));
        }

        new AnimationTimer() {
            public void handle(long currentNanoTime) {
                // Clear the canvas
                gc.clearRect(0, 0, 512, 820);

                for (Lasser l : allLassers) {
                    // if the current object is still ok
                    if (!l.isDestroyed()) {
                        // draw it
                        gc.fillRect(l.getxPos(), l.getyPos(), l.getWidth(), l.getHeight());
                    }
                }

                // remove all destroyed object
                for (int i = allLassers.size() - 1; i >= 0; i--) {
                    if (allLassers.get(i).isDestroyed()) {
                        allLassers.remove(i);
                    }
                }
            }
        }.start();

        stage.show();
    }
}

Lesser class

import javafx.concurrent.Task;
import javafx.scene.shape.Rectangle;

public class Lasser extends Rectangle {

    private volatile boolean isDestroyed = false;

    private double xPos;
    private double yPos;

    public Lasser(double x, double y, long time) {
        super(x, y, 5, 20);
        this.xPos = x;
        this.yPos = y;
        startSelfDestruct(time);
    }

    private void startSelfDestruct(long time) {

        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() {
                try {
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                }
                return null;
            }
        };

        task.setOnSucceeded(e -> {
            isDestroyed = true;
        });

        new Thread(task).start();
    }

    public void move(double x, double y) {
        this.xPos = x;
        this.yPos = y;
    }

    public boolean isDestroyed() {
        return isDestroyed;
    }

    public double getxPos() {
        return xPos;
    }

    public double getyPos() {
        this.yPos -= 1;
        return yPos;
    }

}
JKostikiadis
  • 2,847
  • 2
  • 22
  • 34
  • This looks like a very good idea! So basically you are suggesting that I start another thread that sleeps until the determined destruction time, when it changes the boolean value to true? – Ptolemorphism Oct 15 '17 at 21:40
  • Exactly. Of course you will need to create a new thread for each game object so a good place to do that would be in the constructor on the object for example new Laser(..., 3000) ; where 3000 is the destruction time and in the constructor just start the new Thread with that sleep time. – JKostikiadis Oct 15 '17 at 21:44
  • Nice... Which interface/class do you suggest? I feel like Task, Runnable, Worker, Thread are all available options... – Ptolemorphism Oct 15 '17 at 21:51
  • If you are not going to apply any animation which need to be in the JavaFX thread and you only update the isDestroyed flag then using new Thread(new Runnable(){...}); would be enough but any other framework would do the trick ;). To be honest I would prefer Task. – JKostikiadis Oct 15 '17 at 21:54
  • 3
    You don’t need a separate thread for each object—that would be wasteful. You would be fine with a single event loop thread. Just put the timed events in a priority queue (sorted by due time), and sleep until the next nearest event. Wake up when it’s time to process it, do what you need to do, check the next item in the queue, and if it’s not time to deal with it yet, go back to sleep. – Mike Strobel Oct 15 '17 at 22:30
  • @MikeStrobel You are right, to be honest I wasn't trying to make something optimal. In addition to your suggestion I believe that if the duration is always fixed we don't really need a priority queue cause each new element will be place at the end of the data structure and we are only checking for the first element and removing it if it's time. So for the data structure I would suggest a LinkedList to have the minimum remove append and get ( first element ) time. Of course If i am wrong I would like to know your opinion. – JKostikiadis Oct 15 '17 at 22:46
  • @MikeStrobel I don't really understand how you can use one thread for different laser objects... Could you explain it to me? – Ptolemorphism Oct 15 '17 at 23:17
  • 3
    You can also consider, e.g., using a `PauseTransition` and destroying the object (calling `setDestroyed()`) in the `onFinished` handler. This also avoids using any additional threads and having to be concerned about accessing variables from multiple threads. – James_D Oct 15 '17 at 23:18
  • @James_D When an object is destroyed, what happens to the Collections that contain that object? Does the object become null in the previous index? – Ptolemorphism Oct 15 '17 at 23:31
  • @Ptolemorphism No, an object will remain in a collection unless you remove it. To clarify, by "destroy" I merely referred to setting the `isDestroyed` flag to true in the code in this answer. – James_D Oct 15 '17 at 23:40
  • @JKostikiadis Answering that is a bit much for a comment, so I put it in an answer. – Mike Strobel Oct 16 '17 at 13:04