java游戏如何构造定时effect/fixed距离效果?
java game how to construct timed effect/fixed distance effect?
我正在 javafx 中编写 java 游戏,但我认为这个问题的解决方案并非 javafx 独有...
我有一个实体 class 和它的一堆子class,例如导弹、激光等。但是,当游戏中的角色创建导弹和激光时,它们始终保持 运行 直到它们碰到 canvas 的矩形边界或当它们碰到一个字符并消失时。
但是,我希望 missiles/lasers 可以有许多其他行为:
- 导弹可以一直飞到4秒后消失
- 导弹在飞行600像素后可以一直飞行直到消失(这与上一个类似)
- 激光可以存在于矩形区域中,也可以随角色移动,直到 4 秒后消失。
问题来了,如何实现这种定时效果呢? (也许是 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();
}
更新[看起来不错:)] :
好吧,我不是游戏行业的专家,但这是我的建议:
- 有一个显示对象状态的变量(布尔值)。
private volatile boolean isDestroyed = false
;
注意:volatile是必须的!
- 如果游戏对象有时间限制(自毁),那么在其构造函数中创建时(或通常在其 class 中)启动一个任务,在您想要的持续时间内休眠,并在结束时休眠任务将 isDestroyed 变量更新为 true。
注意:你也可以在Task里面放一个游戏对象销毁的动画。
- 在执行所有渲染操作的主要 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;
}
}
在评论中,您问我如何使用单个线程来管理多个实体的定时销毁。首先,我们不要从使用线程的角度考虑。我们真正想做什么?我们要执行延时操作。
我们该怎么做?嗯...
事件循环调度简介
我们可以创建一个调度程序来在特定时间执行操作(或响应事件)。这些可能是一次性操作,或以固定间隔重复的重复操作。您最终可能会在游戏中执行许多此类操作。我们如何实施呢?好吧,我们实际上 不必 ;已经做过很多次了,而且做得很好。但它的主旨是这样的:
- 创建一个队列来保存计划的工作项目。这应该是一个优先级队列,排序后发生时间最近的工作项最接近队列的前面。
- 启动在队列中等待的处理线程。
- 各种游戏组件通过将工作项插入队列(以线程安全的方式)来安排工作项。插入后,它们会通知处理线程唤醒并检查队列。
- 处理线程在醒来时检查队列中是否有工作项。如果它找到一个,它会查看它的完成时间并将其与当前游戏时间进行比较。
- 如果到了 运行 工作项的时间,它就会这样做。对队列中的下一个项目重复步骤 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' 相当于取消该操作。在上面的示例中,这有两个目的:
- 如果实体被其他方式摧毁,例如,玩家射击并摧毁导弹,您可以简单地调用
destroyNow()
,这反过来会取消任何先前计划的摧毁(现在是多余的) .
- 如果想改变销毁时间,可以再调用
destroyAfter
一次,如果原定的动作还没有发生,将被取消并阻止 运行ning.
注意事项
游戏是一个有趣的案例,特别是关于 时间。考虑:
游戏中的时间不一定以恒定的速度进行。当游戏性能不佳时,时间流通常会相应减慢。也(通常)可以暂停时间。
一个游戏可能依赖多个'clock'。玩家可以暂停游戏,有效冻结 'game time' 时钟。同时,玩家仍然可以与游戏菜单和选项屏幕进行交互,这些屏幕可能会根据 'real' 时间(例如系统时间)进行动画处理。
游戏时间通常是单向流动的,而系统时间则不是。如今,大多数 PC 都将其系统时钟与时间服务器保持同步,因此系统时间会不断被更正为 'drift'。因此,系统时钟及时向后跳转并不罕见。
由于系统时间容易波动,所以并不平稳。但是,在 运行 调整我们的代码时,我们受制于系统调度程序。如果我们将游戏时间设定为每秒增加 'tick' 60 次(以达到 60fps 为目标),我们需要了解我们的 'ticks' 几乎永远不会 完全 [=77] =] 当我们希望他们这样做时。因此,我们应该 interpolate:如果我们的 tick 发生在我们预期的时间之前或之后,我们应该将我们的游戏时间提前稍微少于或多于一个 'tick'.
这些考虑因素可能会阻止您使用第三方调度程序。您可能仍会在开发早期使用一个,但最终您将需要一个根据 游戏时间 而不是 系统时间 进行的。 RxJava 其实有一个叫 TestScheduler
的 scheduler implementation 是受外部时钟控制的。但是,它不是线程安全的,并且依赖于外部参与者手动推进时间,但您可以将其用作您自己的调度程序的模型。
我正在 javafx 中编写 java 游戏,但我认为这个问题的解决方案并非 javafx 独有...
我有一个实体 class 和它的一堆子class,例如导弹、激光等。但是,当游戏中的角色创建导弹和激光时,它们始终保持 运行 直到它们碰到 canvas 的矩形边界或当它们碰到一个字符并消失时。
但是,我希望 missiles/lasers 可以有许多其他行为:
- 导弹可以一直飞到4秒后消失
- 导弹在飞行600像素后可以一直飞行直到消失(这与上一个类似)
- 激光可以存在于矩形区域中,也可以随角色移动,直到 4 秒后消失。
问题来了,如何实现这种定时效果呢? (也许是 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();
}
更新[看起来不错:)] :
好吧,我不是游戏行业的专家,但这是我的建议:
- 有一个显示对象状态的变量(布尔值)。
private volatile boolean isDestroyed = false
;
注意:volatile是必须的!
- 如果游戏对象有时间限制(自毁),那么在其构造函数中创建时(或通常在其 class 中)启动一个任务,在您想要的持续时间内休眠,并在结束时休眠任务将 isDestroyed 变量更新为 true。
注意:你也可以在Task里面放一个游戏对象销毁的动画。
- 在执行所有渲染操作的主要 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;
}
}
在评论中,您问我如何使用单个线程来管理多个实体的定时销毁。首先,我们不要从使用线程的角度考虑。我们真正想做什么?我们要执行延时操作。
我们该怎么做?嗯...
事件循环调度简介
我们可以创建一个调度程序来在特定时间执行操作(或响应事件)。这些可能是一次性操作,或以固定间隔重复的重复操作。您最终可能会在游戏中执行许多此类操作。我们如何实施呢?好吧,我们实际上 不必 ;已经做过很多次了,而且做得很好。但它的主旨是这样的:
- 创建一个队列来保存计划的工作项目。这应该是一个优先级队列,排序后发生时间最近的工作项最接近队列的前面。
- 启动在队列中等待的处理线程。
- 各种游戏组件通过将工作项插入队列(以线程安全的方式)来安排工作项。插入后,它们会通知处理线程唤醒并检查队列。
- 处理线程在醒来时检查队列中是否有工作项。如果它找到一个,它会查看它的完成时间并将其与当前游戏时间进行比较。
- 如果到了 运行 工作项的时间,它就会这样做。对队列中的下一个项目重复步骤 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' 相当于取消该操作。在上面的示例中,这有两个目的:
- 如果实体被其他方式摧毁,例如,玩家射击并摧毁导弹,您可以简单地调用
destroyNow()
,这反过来会取消任何先前计划的摧毁(现在是多余的) . - 如果想改变销毁时间,可以再调用
destroyAfter
一次,如果原定的动作还没有发生,将被取消并阻止 运行ning.
注意事项
游戏是一个有趣的案例,特别是关于 时间。考虑:
游戏中的时间不一定以恒定的速度进行。当游戏性能不佳时,时间流通常会相应减慢。也(通常)可以暂停时间。
一个游戏可能依赖多个'clock'。玩家可以暂停游戏,有效冻结 'game time' 时钟。同时,玩家仍然可以与游戏菜单和选项屏幕进行交互,这些屏幕可能会根据 'real' 时间(例如系统时间)进行动画处理。
游戏时间通常是单向流动的,而系统时间则不是。如今,大多数 PC 都将其系统时钟与时间服务器保持同步,因此系统时间会不断被更正为 'drift'。因此,系统时钟及时向后跳转并不罕见。
由于系统时间容易波动,所以并不平稳。但是,在 运行 调整我们的代码时,我们受制于系统调度程序。如果我们将游戏时间设定为每秒增加 'tick' 60 次(以达到 60fps 为目标),我们需要了解我们的 'ticks' 几乎永远不会 完全 [=77] =] 当我们希望他们这样做时。因此,我们应该 interpolate:如果我们的 tick 发生在我们预期的时间之前或之后,我们应该将我们的游戏时间提前稍微少于或多于一个 'tick'.
这些考虑因素可能会阻止您使用第三方调度程序。您可能仍会在开发早期使用一个,但最终您将需要一个根据 游戏时间 而不是 系统时间 进行的。 RxJava 其实有一个叫 TestScheduler
的 scheduler implementation 是受外部时钟控制的。但是,它不是线程安全的,并且依赖于外部参与者手动推进时间,但您可以将其用作您自己的调度程序的模型。