【问题标题】:Breaking recursion from listeners打破听众的递归
【发布时间】:2016-09-08 16:20:54
【问题描述】:

我试图打破使此代码递归的特殊情况。

我有一个 Javafx 游戏,其中有人类和计算机玩家在轮到他的时候玩,并且可能有很多轮。

计算机应该自动播放并立即移动到下一个播放器,并且不会直接向 UI 显示任何指示(但可以查看它之后的操作)。

问题是在只有电脑玩家的情况下,我们会在加载currentBoardPane的那一刻来到这里,进入所有玩家都是电脑的条件,设置下一个玩家的棋盘,然后没有完成调用,再次调用相同的函数:

    currentBoardPane.addListener((e) -> {  
        if(gameManager.checkIfCurrentPlayerIsComputer()){

            gameManager.playAutoMovesForCurrentPlayer();
            gameManager.setNextPlayer(); // it does current player property = next player

            //update board on scene
            currentBoardPaneIndex = ((currentBoardPaneIndex + 1) % gameManager.getPlayers().size());
            currentBoardPane.setValue(boardPanes.get((currentBoardPaneIndex))); //this is a recursive call
        }
    });

如果我订阅了GameManager 中的currentPlayer 属性的侦听器,那么我仍然需要从该侦听器调用setNextPlayer(),而这又是递归的。

如果所有玩家都是计算机,我可以做一个特殊情况,然后从 while(true){} 运行游戏而不是侦听器和绑定,但必须有更好的方法来打破这种递归。

有没有办法在仍然有侦听器和绑定的同时不进入递归?

注意事项:

currentBoardPane 表示屏幕上的当前游戏板,它是ObjectProperty

【问题讨论】:

  • 如果 playAutoMovesForCurrentPlayer() 方法花费了很长时间(例如,您正在下棋,或其他需要大量计算移动的游戏),那么您将调用 playAutoMovesForCurrentPlayer()一个Task,在后台线程上执行它,并在onSucceeded处理程序中执行剩余的代码(setNextPlayer(),更新索引和窗格)。这会破坏递归,因为您的处理程序不会直接调用更改。如果您知道移动接近瞬时,这可能不是一个理想的解决方案,但它会解决问题。
  • 这是一个简单的游戏,“AI”只是随机选择,所以它是即时的。
  • 是的,但它仍然可以解决问题。如果我理解正确,每个动作都会更新 UI,对吗?因此,如果您使用了您提出的解决方案 (while (true) { doMoves(); }),则必须将其放在后台线程中并使用 Platform.runLater(...) 更新 UI,否则您将看不到 UI 更改(直到循环完成,看起来像它永远不会发生)。因此,无论哪种方式,您都需要使用后台线程。
  • while 循环将调用使用事件或侦听器更新 UI 的方法,但它仍然会阻塞 javafx 主线程直到它完成?那么我可能更喜欢使用你的第一个建议。
  • 如果它在 FX 应用程序线程上运行,那么是的,它将阻塞该线程直到它完成。 (如果不是,你已经在创建一个后台线程,所以无论哪种方式你都在使用线程。)

标签: java recursion javafx java-8 javafx-8


【解决方案1】:

对您的代码做出以下假设:

  1. 目前一切都在 FX 应用程序线程上运行
  2. currentBoardPane.setValue(...) 会导致 UI 更新(因此您每次移动都会更新 UI)

那么一个“快速而肮脏”的方法是:

currentBoardPane.addListener((e) -> {  
    if(gameManager.checkIfCurrentPlayerIsComputer()){

        gameManager.playAutoMovesForCurrentPlayer();

        //update board on scene
        Platform.runLater(() -> {
            gameManager.setNextPlayer(); // it does current player property = next player
            currentBoardPaneIndex = ((currentBoardPaneIndex + 1) % gameManager.getPlayers().size());
            currentBoardPane.setValue(boardPanes.get((currentBoardPaneIndex))); //this is a recursive call
        });
    }
});

这会将更新委托给新的Runnable,调度可运行在 FX 应用程序线程上执行,并立即退出处理程序。因此对currentBoardPane.setValue(...) 的调用稍后执行,不再递归。

事实上,如果你多做一点工作:

private final Executor aiExecutor = Executors.newSingleThreadExecutor();

// ...

currentBoardPane.addListener((e) -> {  
    if(gameManager.checkIfCurrentPlayerIsComputer()){

        Task<Void> makeMoveTask = new Task<Void>() {
            @Override
            protected Void call() {
                gameManager.playAutoMovesForCurrentPlayer();
                return null ;
            }
        };

        makeMoveTask.setOnSucceeded(e -> {

            //update board on scene
            gameManager.setNextPlayer(); // it does current player property = next player
            currentBoardPaneIndex = ((currentBoardPaneIndex + 1) % gameManager.getPlayers().size());
            currentBoardPane.setValue(boardPanes.get((currentBoardPaneIndex))); //this is a recursive call
        });

        aiExecutor.execute(makeMoveTask);
    }
});

如果计算移动需要足够的时间,以至于在移动发生时阻止 UI 是不可接受的,那么这正是您将使用的代码。 (如果计算移动需要很少的时间,这仍然可以正常工作。)这假设 playAutoMovesForCurrentPlayer() 不更新 UI。

【讨论】:

  • 好吧,playAutoMovesForCurrentPlayer() 在控制器中确实有更新 UI 的事件或侦听器...
  • 所以你可以只使用第一个选项。可能尽管将计算移动的逻辑分离出来会更清晰,尽管这很简单,即从采用移动表示并用它更新 UI 的逻辑中分离出来。
  • 再次感谢您的帮助。
猜你喜欢
  • 1970-01-01
  • 2012-06-02
  • 2022-10-24
  • 2021-07-08
  • 1970-01-01
  • 1970-01-01
  • 2017-04-09
  • 2014-08-22
  • 1970-01-01
相关资源
最近更新 更多