Scala Swing 跳过框架的重绘
Scala Swing skips repaint of the Frame
我目前正在使用 Scala 实现黑白棋游戏,到目前为止运行良好。虽然在实现 GUI 时(使用 Scala Swing),我偶然发现了一个我似乎无法解决的问题。
在与电脑对手对战时,框架似乎只有在机器人完成移动后才会重新绘制。
该游戏也可以通过终端玩,这样做每次都会正确更新帧,无论玩家配置如何(玩家对玩家或玩家对计算机)。使用 GUI 进行玩家对战完全没有问题。
这可能是我的疏忽,但到目前为止我无法找到解决方案,非常感谢任何帮助。
到目前为止,我已经尝试了各种组合,包括重新验证和重新绘制各个面板、添加和删除侦听器、将反应器模式的实现更改为 Scala Swing 提供的模式、添加 Thread.sleep 以查看是否存在可能是某种调度冲突。
import java.awt.Color
import othello.controller.Controller
import javax.swing.ImageIcon
import javax.swing.border.LineBorder
import scala.swing.event.MouseClicked
import scala.swing.{BorderPanel, BoxPanel, Dimension, FlowPanel, GridPanel, Label, Orientation}
class TablePanel(controller: Controller) extends FlowPanel {
val sides = 32
val sidesColor: Color = Color.lightGray
val squareSize = 52
def tableSize: Int = controller.board.size
def edgeLength: Int = tableSize * squareSize
def rows: BoxPanel = new BoxPanel(Orientation.Vertical) {
background = sidesColor
preferredSize = new Dimension(sides, edgeLength)
contents += new Label {
preferredSize = new Dimension(sides, sides)
}
contents += new GridPanel(tableSize, 1) {
background = sidesColor
for { i <- 1 to rows } contents += new Label(s"$i")
}
}
def columns: GridPanel = new GridPanel(1, tableSize) {
background = sidesColor
preferredSize = new Dimension(edgeLength, sides)
for { i <- 0 until columns } contents += new Label(s"${(i + 65).toChar}")
}
def table: GridPanel = new GridPanel(tableSize, tableSize) {
background = new Color(10, 90, 10)
for {
col <- 0 until columns
row <- 0 until rows
} contents += square(col, row)
}
def square(row: Int, col: Int): Label = new Label {
border = new LineBorder(new Color(30, 30, 30, 140), 1)
preferredSize = new Dimension(squareSize, squareSize)
icon = controller.board.valueOf(col, row) match {
case -1 => new ImageIcon("resources/big_dot.png")
case 0 => new ImageIcon("resources/empty.png")
case 1 => new ImageIcon("resources/black_shadow.png")
case 2 => new ImageIcon("resources/white_shadow.png")
}
listenTo(mouse.clicks)
reactions += {
case _: MouseClicked =>
if (controller.options.contains((col, row))) controller.set(col, row)
else if (controller.board.gameOver) controller.newGame()
else controller.highlight()
}
}
def redraw(): Unit = {
contents.clear
contents += new BorderPanel {
add(rows, BorderPanel.Position.West)
add(new BoxPanel(Orientation.Vertical) {
contents += columns
contents += table
}, BorderPanel.Position.East)
}
repaint
}
}
import scala.swing._
import othello.controller._
import othello.util.Observer
import scala.swing.event.Key
class SwingGui(controller: Controller) extends Frame with Observer {
controller.add(this)
lazy val tablePanel = new TablePanel(controller)
lazy val mainFrame: MainFrame = new MainFrame {
title = "Othello"
menuBar = menus
contents = tablePanel
centerOnScreen
// peer.setAlwaysOnTop(true)
resizable = false
visible = true
}
def menus: MenuBar = new MenuBar {
contents += new Menu("File") {
mnemonic = Key.F
contents += new MenuItem(Action("New Game") {
controller.newGame()
})
contents += new MenuItem(Action("Quit") {
controller.exit()
})
}
contents += new Menu("Edit") {
mnemonic = Key.E
contents += new MenuItem(Action("Undo") {
controller.undo()
})
contents += new MenuItem(Action("Redo") {
controller.redo()
})
}
contents += new Menu("Options") {
mnemonic = Key.O
contents += new MenuItem(Action("Highlight possible moves") {
controller.highlight()
})
contents += new MenuItem(Action("Reduce board size") {
controller.resizeBoard("-")
})
contents += new MenuItem(Action("Increase board size") {
controller.resizeBoard("+")
})
contents += new MenuItem(Action("Reset board size") {
controller.resizeBoard(".")
})
contents += new Menu("Game mode") {
contents += new MenuItem(Action("Player vs. Computer") {
controller.setupPlayers("1")
})
contents += new MenuItem(Action("Player vs. Player") {
controller.setupPlayers("2")
})
}
}
}
def update: Boolean = {
tablePanel.redraw()
mainFrame.pack
mainFrame.centerOnScreen
mainFrame.repaint
true
}
}
预期的行为是每次都重新绘制框架。实际结果是Frame只是在对方下手后才被重新绘制。只有通过单击 UI.
专门玩玩家 vs 机器人时才会发生
我不认为问题出在你显示的代码中,但我敢打赌你用计算机的 AI 计算阻塞了 "event dispatch thread"(UI 线程)播放器。
在 Swing 应用程序中,有一个名为 "event dispatch thread" 的特殊线程负责处理来自 O/S 的消息,包括处理重绘消息。所有 UI 事件处理程序都将在此线程上调用。如果您使用该线程进行任何需要很长时间的计算(例如计算机在这样的游戏中移动),任何 UI 更新都将被阻止,直到线程空闲。
本教程包含更多信息:https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html
您需要将 AI 移至后台线程并释放事件调度线程以处理重绘。如果您不熟悉 multi-threaded 程序,这可能很难实施!祝你好运。
正如 Rich 所建议的,解决方案是并发。使搜索算法成为 Future-type 现在解决了这个问题。它可能不是最干净的实现,因为这是我第一次使用 Future,但这就是它现在的样子:
def selectAndSet(): Future[_] = Future(if (!board.gameOver && player.isBot) {
new MoveSelector(this).select() match {
case Success(square) => set(square)
case _ => omitPlayer()
}
selectAndSet()
})(ExecutionContext.global)
我目前正在使用 Scala 实现黑白棋游戏,到目前为止运行良好。虽然在实现 GUI 时(使用 Scala Swing),我偶然发现了一个我似乎无法解决的问题。
在与电脑对手对战时,框架似乎只有在机器人完成移动后才会重新绘制。
该游戏也可以通过终端玩,这样做每次都会正确更新帧,无论玩家配置如何(玩家对玩家或玩家对计算机)。使用 GUI 进行玩家对战完全没有问题。
这可能是我的疏忽,但到目前为止我无法找到解决方案,非常感谢任何帮助。
到目前为止,我已经尝试了各种组合,包括重新验证和重新绘制各个面板、添加和删除侦听器、将反应器模式的实现更改为 Scala Swing 提供的模式、添加 Thread.sleep 以查看是否存在可能是某种调度冲突。
import java.awt.Color
import othello.controller.Controller
import javax.swing.ImageIcon
import javax.swing.border.LineBorder
import scala.swing.event.MouseClicked
import scala.swing.{BorderPanel, BoxPanel, Dimension, FlowPanel, GridPanel, Label, Orientation}
class TablePanel(controller: Controller) extends FlowPanel {
val sides = 32
val sidesColor: Color = Color.lightGray
val squareSize = 52
def tableSize: Int = controller.board.size
def edgeLength: Int = tableSize * squareSize
def rows: BoxPanel = new BoxPanel(Orientation.Vertical) {
background = sidesColor
preferredSize = new Dimension(sides, edgeLength)
contents += new Label {
preferredSize = new Dimension(sides, sides)
}
contents += new GridPanel(tableSize, 1) {
background = sidesColor
for { i <- 1 to rows } contents += new Label(s"$i")
}
}
def columns: GridPanel = new GridPanel(1, tableSize) {
background = sidesColor
preferredSize = new Dimension(edgeLength, sides)
for { i <- 0 until columns } contents += new Label(s"${(i + 65).toChar}")
}
def table: GridPanel = new GridPanel(tableSize, tableSize) {
background = new Color(10, 90, 10)
for {
col <- 0 until columns
row <- 0 until rows
} contents += square(col, row)
}
def square(row: Int, col: Int): Label = new Label {
border = new LineBorder(new Color(30, 30, 30, 140), 1)
preferredSize = new Dimension(squareSize, squareSize)
icon = controller.board.valueOf(col, row) match {
case -1 => new ImageIcon("resources/big_dot.png")
case 0 => new ImageIcon("resources/empty.png")
case 1 => new ImageIcon("resources/black_shadow.png")
case 2 => new ImageIcon("resources/white_shadow.png")
}
listenTo(mouse.clicks)
reactions += {
case _: MouseClicked =>
if (controller.options.contains((col, row))) controller.set(col, row)
else if (controller.board.gameOver) controller.newGame()
else controller.highlight()
}
}
def redraw(): Unit = {
contents.clear
contents += new BorderPanel {
add(rows, BorderPanel.Position.West)
add(new BoxPanel(Orientation.Vertical) {
contents += columns
contents += table
}, BorderPanel.Position.East)
}
repaint
}
}
import scala.swing._
import othello.controller._
import othello.util.Observer
import scala.swing.event.Key
class SwingGui(controller: Controller) extends Frame with Observer {
controller.add(this)
lazy val tablePanel = new TablePanel(controller)
lazy val mainFrame: MainFrame = new MainFrame {
title = "Othello"
menuBar = menus
contents = tablePanel
centerOnScreen
// peer.setAlwaysOnTop(true)
resizable = false
visible = true
}
def menus: MenuBar = new MenuBar {
contents += new Menu("File") {
mnemonic = Key.F
contents += new MenuItem(Action("New Game") {
controller.newGame()
})
contents += new MenuItem(Action("Quit") {
controller.exit()
})
}
contents += new Menu("Edit") {
mnemonic = Key.E
contents += new MenuItem(Action("Undo") {
controller.undo()
})
contents += new MenuItem(Action("Redo") {
controller.redo()
})
}
contents += new Menu("Options") {
mnemonic = Key.O
contents += new MenuItem(Action("Highlight possible moves") {
controller.highlight()
})
contents += new MenuItem(Action("Reduce board size") {
controller.resizeBoard("-")
})
contents += new MenuItem(Action("Increase board size") {
controller.resizeBoard("+")
})
contents += new MenuItem(Action("Reset board size") {
controller.resizeBoard(".")
})
contents += new Menu("Game mode") {
contents += new MenuItem(Action("Player vs. Computer") {
controller.setupPlayers("1")
})
contents += new MenuItem(Action("Player vs. Player") {
controller.setupPlayers("2")
})
}
}
}
def update: Boolean = {
tablePanel.redraw()
mainFrame.pack
mainFrame.centerOnScreen
mainFrame.repaint
true
}
}
预期的行为是每次都重新绘制框架。实际结果是Frame只是在对方下手后才被重新绘制。只有通过单击 UI.
专门玩玩家 vs 机器人时才会发生我不认为问题出在你显示的代码中,但我敢打赌你用计算机的 AI 计算阻塞了 "event dispatch thread"(UI 线程)播放器。
在 Swing 应用程序中,有一个名为 "event dispatch thread" 的特殊线程负责处理来自 O/S 的消息,包括处理重绘消息。所有 UI 事件处理程序都将在此线程上调用。如果您使用该线程进行任何需要很长时间的计算(例如计算机在这样的游戏中移动),任何 UI 更新都将被阻止,直到线程空闲。
本教程包含更多信息:https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html
您需要将 AI 移至后台线程并释放事件调度线程以处理重绘。如果您不熟悉 multi-threaded 程序,这可能很难实施!祝你好运。
正如 Rich 所建议的,解决方案是并发。使搜索算法成为 Future-type 现在解决了这个问题。它可能不是最干净的实现,因为这是我第一次使用 Future,但这就是它现在的样子:
def selectAndSet(): Future[_] = Future(if (!board.gameOver && player.isBot) {
new MoveSelector(this).select() match {
case Success(square) => set(square)
case _ => omitPlayer()
}
selectAndSet()
})(ExecutionContext.global)