Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

java game how to construct timed effect/fixed distance effect?

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

like image 605
Ptolemorphism Avatar asked Aug 09 '26 01:08

Ptolemorphism


2 Answers

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.

like image 72
Mike Strobel Avatar answered Aug 11 '26 16:08

Mike Strobel


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;
    }

}
like image 24
JKostikiadis Avatar answered Aug 11 '26 14:08

JKostikiadis



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!