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)