在动画模拟中将逻辑与图形分离
Separating logic from graphics in animated simulation
我需要编写一个程序来模拟某个过程的动画。不详细讲,会有蜗牛(讲师的想法)在类似棋盘的表面上移动,其中每个矩形(我称之为单元格)都有(x,y)坐标。
我无法将此模拟的逻辑与图形分开。例如:
我有一个 Snail
class。它存储蜗牛的坐标并计算其行为。当它确定蜗牛应该从 (x, y) 移动到 (a, b) 时,我需要为该移动设置动画,因此我还必须以像素为单位计算蜗牛的位置,并且我需要在一段时间内重复执行此操作以使蜗牛能够流畅地移动,而不是跳跃。如果不是因为我不想在 Snail
class 中这样做,这不会是一个问题,因为它是严格的图形相关的,与逻辑没有任何关系。
我不能只做一个循环来根据棋盘上的坐标绘制 Snail
s,因为它不能反映它的流畅运动,只反映当前位置。
我现在最好的想法是通过 GraphicSnail
扩展 Snail
,这将另外计算和存储属性,例如蜗牛的像素位置,但对我来说这似乎不够独立。
提前感谢您的帮助。
您可能想要使用 Observer pattern。
使用中间接口进行分离:
public interface SnailObserver {
void update(Snail snail);
}
然后让你的图形相关class实现这个接口。我不知道您使用什么库(如果有的话)来渲染图形。如果您使用的是 JavaFX 之类的东西,GraphicSnail
class 也可以继承自 ImageView
或其他东西。
public class GraphicSnail implements SnailObserver {
@Overrride
public void update(Snail snail) {
// Use snail.getX() and snail.getY() to obtain
// position of the snail and perform whatever
// graphical updates you wish to make
}
}
最后,这就是 Snail
class 的样子。请注意附加字段,它包含对 GraphicSnail
的引用,但通过 SnailObserver
接口进行引用。这就是分离所在。请注意,您还可以存储此类观察者的完整列表。在任何情况下,关键部分是每当 Snail
对象的状态发生变化时调用观察者对象的 update()
方法,从而使观察者意识到某些事情发生了变化。然后观察者对象检查 Snail
对象的当前状态并相应地修改自己的状态。
public class Snail {
private double x;
private double y;
private SnailObserver observer;
public void move() {
// Move the snail and then notify the observer
// that the snail has changed like so:
observer.update(this);
}
public void registerObserver(SnailObserver observer) {
this.observer = observer;
observer.update(this); // initial sync
}
public double getX() {
return x;
}
public double getY() {
return y;
}
}
最后,不要忘记在创建 Snail
和 GraphicSnail
对象后注册观察者:
Snail snail = new Snail();
GraphicSnail graphicSnail = new GraphicSnail();
snail.registerObserver(graphicSnail);
希望这对您有所帮助,
斯捷潘
与 Stephan 的回答类似,在您的模型 classes 中实施 observable properties 进行模拟。创建单独的皮肤 classes 负责可观察模型 classes 的视觉表示。在创建皮肤时为皮肤分配对模型的引用,并在皮肤中将侦听器添加到模型的可侦听属性,以便皮肤可以适当地对模型状态变化做出反应。
有关此方法的示例,请参阅此 Tic-Tac-Toe code 中的 Square 和 SquareSkin 以及 Board 和 BoardSkin class。以下是摘录:
class Square {
enum State { EMPTY, NOUGHT, CROSS }
private final SquareSkin skin;
private ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.EMPTY);
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
public State getState() {
return state.get();
}
private final Game game;
public Square(Game game) {
this.game = game;
skin = new SquareSkin(this);
}
public void pressed() {
if (!game.isGameOver() && state.get() == State.EMPTY) {
state.set(game.getCurrentPlayer());
game.boardUpdated();
game.nextTurn();
}
}
public Node getSkin() {
return skin;
}
}
class SquareSkin extends StackPane {
static final Image noughtImage = new Image(
"http://icons.iconarchive.com/icons/double-j-design/origami-colored-pencil/128/green-cd-icon.png"
);
static final Image crossImage = new Image(
"http://icons.iconarchive.com/icons/double-j-design/origami-colored-pencil/128/blue-cross-icon.png"
);
private final ImageView imageView = new ImageView();
SquareSkin(final Square square) {
getStyleClass().add("square");
imageView.setMouseTransparent(true);
getChildren().setAll(imageView);
setPrefSize(crossImage.getHeight() + 20, crossImage.getHeight() + 20);
setOnMousePressed(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
square.pressed();
}
});
square.stateProperty().addListener(new ChangeListener<Square.State>() {
@Override public void changed(ObservableValue<? extends Square.State> observableValue, Square.State oldState, Square.State state) {
switch (state) {
case EMPTY: imageView.setImage(null); break;
case NOUGHT: imageView.setImage(noughtImage); break;
case CROSS: imageView.setImage(crossImage); break;
}
}
});
}
}
使用模型 classes 和皮肤 classes 是创建 JavaFX UI 控件的方式。如果您查看控件 classes 的实现及其在 JavaFX source code base 中的外观,您可以了解其工作原理。
总的来说,我认为通过扩展 Control and using the inbuilt SkinBase class 创建控件对大多数应用程序来说都是多余的。但是,您可以根据您的需要评估该方法。在之前链接的 Tic-Tac-Toe 代码中,您可能最好使用更简单的方法,例如观察者到皮肤的方法。
另外,根据你想用这个走多远,你可以分开 layout definition from code using FXML and style from code using CSS stylesheets。我建议在大多数情况下使用样式表。对于您描述的情况,可能不需要 FXML 分离。
我需要编写一个程序来模拟某个过程的动画。不详细讲,会有蜗牛(讲师的想法)在类似棋盘的表面上移动,其中每个矩形(我称之为单元格)都有(x,y)坐标。
我无法将此模拟的逻辑与图形分开。例如:
我有一个 Snail
class。它存储蜗牛的坐标并计算其行为。当它确定蜗牛应该从 (x, y) 移动到 (a, b) 时,我需要为该移动设置动画,因此我还必须以像素为单位计算蜗牛的位置,并且我需要在一段时间内重复执行此操作以使蜗牛能够流畅地移动,而不是跳跃。如果不是因为我不想在 Snail
class 中这样做,这不会是一个问题,因为它是严格的图形相关的,与逻辑没有任何关系。
我不能只做一个循环来根据棋盘上的坐标绘制 Snail
s,因为它不能反映它的流畅运动,只反映当前位置。
我现在最好的想法是通过 GraphicSnail
扩展 Snail
,这将另外计算和存储属性,例如蜗牛的像素位置,但对我来说这似乎不够独立。
提前感谢您的帮助。
您可能想要使用 Observer pattern。
使用中间接口进行分离:
public interface SnailObserver {
void update(Snail snail);
}
然后让你的图形相关class实现这个接口。我不知道您使用什么库(如果有的话)来渲染图形。如果您使用的是 JavaFX 之类的东西,GraphicSnail
class 也可以继承自 ImageView
或其他东西。
public class GraphicSnail implements SnailObserver {
@Overrride
public void update(Snail snail) {
// Use snail.getX() and snail.getY() to obtain
// position of the snail and perform whatever
// graphical updates you wish to make
}
}
最后,这就是 Snail
class 的样子。请注意附加字段,它包含对 GraphicSnail
的引用,但通过 SnailObserver
接口进行引用。这就是分离所在。请注意,您还可以存储此类观察者的完整列表。在任何情况下,关键部分是每当 Snail
对象的状态发生变化时调用观察者对象的 update()
方法,从而使观察者意识到某些事情发生了变化。然后观察者对象检查 Snail
对象的当前状态并相应地修改自己的状态。
public class Snail {
private double x;
private double y;
private SnailObserver observer;
public void move() {
// Move the snail and then notify the observer
// that the snail has changed like so:
observer.update(this);
}
public void registerObserver(SnailObserver observer) {
this.observer = observer;
observer.update(this); // initial sync
}
public double getX() {
return x;
}
public double getY() {
return y;
}
}
最后,不要忘记在创建 Snail
和 GraphicSnail
对象后注册观察者:
Snail snail = new Snail();
GraphicSnail graphicSnail = new GraphicSnail();
snail.registerObserver(graphicSnail);
希望这对您有所帮助, 斯捷潘
与 Stephan 的回答类似,在您的模型 classes 中实施 observable properties 进行模拟。创建单独的皮肤 classes 负责可观察模型 classes 的视觉表示。在创建皮肤时为皮肤分配对模型的引用,并在皮肤中将侦听器添加到模型的可侦听属性,以便皮肤可以适当地对模型状态变化做出反应。
有关此方法的示例,请参阅此 Tic-Tac-Toe code 中的 Square 和 SquareSkin 以及 Board 和 BoardSkin class。以下是摘录:
class Square {
enum State { EMPTY, NOUGHT, CROSS }
private final SquareSkin skin;
private ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.EMPTY);
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
public State getState() {
return state.get();
}
private final Game game;
public Square(Game game) {
this.game = game;
skin = new SquareSkin(this);
}
public void pressed() {
if (!game.isGameOver() && state.get() == State.EMPTY) {
state.set(game.getCurrentPlayer());
game.boardUpdated();
game.nextTurn();
}
}
public Node getSkin() {
return skin;
}
}
class SquareSkin extends StackPane {
static final Image noughtImage = new Image(
"http://icons.iconarchive.com/icons/double-j-design/origami-colored-pencil/128/green-cd-icon.png"
);
static final Image crossImage = new Image(
"http://icons.iconarchive.com/icons/double-j-design/origami-colored-pencil/128/blue-cross-icon.png"
);
private final ImageView imageView = new ImageView();
SquareSkin(final Square square) {
getStyleClass().add("square");
imageView.setMouseTransparent(true);
getChildren().setAll(imageView);
setPrefSize(crossImage.getHeight() + 20, crossImage.getHeight() + 20);
setOnMousePressed(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
square.pressed();
}
});
square.stateProperty().addListener(new ChangeListener<Square.State>() {
@Override public void changed(ObservableValue<? extends Square.State> observableValue, Square.State oldState, Square.State state) {
switch (state) {
case EMPTY: imageView.setImage(null); break;
case NOUGHT: imageView.setImage(noughtImage); break;
case CROSS: imageView.setImage(crossImage); break;
}
}
});
}
}
使用模型 classes 和皮肤 classes 是创建 JavaFX UI 控件的方式。如果您查看控件 classes 的实现及其在 JavaFX source code base 中的外观,您可以了解其工作原理。
总的来说,我认为通过扩展 Control and using the inbuilt SkinBase class 创建控件对大多数应用程序来说都是多余的。但是,您可以根据您的需要评估该方法。在之前链接的 Tic-Tac-Toe 代码中,您可能最好使用更简单的方法,例如观察者到皮肤的方法。
另外,根据你想用这个走多远,你可以分开 layout definition from code using FXML and style from code using CSS stylesheets。我建议在大多数情况下使用样式表。对于您描述的情况,可能不需要 FXML 分离。