java游戏如何构造定时effect/fixed距离效果?

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

我正在 javafx 中编写 java 游戏,但我认为这个问题的解决方案并非 javafx 独有...

我有一个实体 class 和它的一堆子class,例如导弹、激光等。但是,当游戏中的角色创建导弹和激光时,它们始终保持 运行 直到它们碰到 canvas 的矩形边界或当它们碰到一个字符并消失时。

但是,我希望 missiles/lasers 可以有许多其他行为:

问题来了,如何实现这种定时效果呢? (也许是 propertyChangeListener?)或者我应该向实体本身添加内容,还是应该考虑更改我的控制器 Class? 这是我的代码:

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

更新[看起来不错:)] :

好吧,我不是游戏行业的专家,但这是我的建议:

  1. 有一个显示对象状态的变量(布尔值)。
    • private volatile boolean isDestroyed = false;

注意:volatile是必须的!

  1. 如果游戏对象有时间限制(自毁),那么在其构造函数中创建时(或通常在其 class 中)启动一个任务,在您想要的持续时间内休眠,并在结束时休眠任务将 isDestroyed 变量更新为 true。

注意:你也可以在Task里面放一个游戏对象销毁的动画。

  1. 在执行所有渲染操作的主要 update() 中,您可以忽略渲染游戏对象或将其从列表中删除(注意避免 ConcurrentModificationException)

编辑: 那么让我们尝试一个例子..下面的代码不是 drawing/moving 形状的最佳方式它只是为了展示我的解决方案。

主要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();
    }
}

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

}

在评论中,您问我如何使用单个线程来管理多个实体的定时销毁。首先,我们不要从使用线程的角度考虑。我们真正想做什么?我们要执行延时操作。

我们该怎么做?嗯...

事件循环调度简介

我们可以创建一个调度程序来在特定时间执行操作(或响应事件)。这些可能是一次性操作,或以固定间隔重复的重复操作。您最终可能会在游戏中执行许多此类操作。我们如何实施呢?好吧,我们实际上 不必 ;已经做过很多次了,而且做得很好。但它的主旨是这样的:

  1. 创建一个队列来保存计划的工作项目。这应该是一个优先级队列,排序后发生时间最近的工作项最接近队列的前面。
  2. 启动在队列中等待的处理线程。
  3. 各种游戏组件通过将工作项插入队列(以线程安全的方式)来安排工作项。插入后,它们会通知处理线程唤醒并检查队列。
  4. 处理线程在醒来时检查队列中是否有工作项。如果它找到一个,它会查看它的完成时间并将其与当前游戏时间进行比较。
    • 如果到了 运行 工作项的时间,它就会这样做。对队列中的下一个项目重复步骤 4,如果有的话。
    • 如果 不到 时间 运行 工作项,线程将自行休眠,直到 dueTime - currentTime 结束。
    • 或者,如果队列为空,则无限期休眠;我们会在安排下一个工作项目时醒来。

这种调度程序称为 事件循环 :它 运行 在 "dequeue, run, wait, repeat" 循环中。在 RxJava 中可以找到一个很好的例子。你可以这样使用它:

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) */
}

要安排一个实体在 4 秒后销毁,您可以调用 entity.destroyAfter(4L, TimeUnit.SECONDS)。该调用将安排在 4 秒延迟后调用 destroyNow() 方法。计划的动作存储在SerialDisposable中,可用于'dispose'某些对象。在这种情况下,我们使用它来跟踪预定的销毁操作,并且 'disposal' 相当于取消该操作。在上面的示例中,这有两个目的:

  1. 如果实体被其他方式摧毁,例如,玩家射击并摧毁导弹,您可以简单地调用 destroyNow(),这反过来会取消任何先前计划的摧毁(现在是多余的) .
  2. 如果想改变销毁时间,可以再调用destroyAfter一次,如果原定的动作还没有发生,将被取消并阻止 运行ning.

注意事项

游戏是一个有趣的案例,特别是关于 时间。考虑:

  1. 游戏中的时间不一定以恒定的速度进行。当游戏性能不佳时,时间流通常会相应减慢。也(通常)可以暂停时间。

  2. 一个游戏可能依赖多个'clock'。玩家可以暂停游戏,有效冻结 'game time' 时钟。同时,玩家仍然可以与游戏菜单和选项屏幕进行交互,这些屏幕可能会根据 'real' 时间(例如系统时间)进行动画处理。

  3. 游戏时间通常是单向流动的,而系统时间则不是。如今,大多数 PC 都将其系统时钟与时间服务器保持同步,因此系统时间会不断被更正为 'drift'。因此,系统时钟及时向后跳转并不罕见。

  4. 由于系统时间容易波动,所以并不平稳。但是,在 运行 调整我们的代码时,我们受制于系统调度程序。如果我们将游戏时间设定为每秒增加 'tick' 60 次(以达到 60fps 为目标),我们需要了解我们的 'ticks' 几乎永远不会 完全 [=77] =] 当我们希望他们这样做时。因此,我们应该 interpolate:如果我们的 tick 发生在我们预期的时间之前或之后,我们应该将我们的游戏时间提前稍微少于或多于一个 'tick'.

这些考虑因素可能会阻止您使用第三方调度程序。您可能仍会在开发早期使用一个,但最终您将需要一个根据 游戏时间 而不是 系统时间 进行的。 RxJava 其实有一个叫 TestSchedulerscheduler implementation 是受外部时钟控制的。但是,它不是线程安全的,并且依赖于外部参与者手动推进时间,但您可以将其用作您自己的调度程序的模型。