5日目 

昨日はゲームの状態への並行処理を管理するために Akka アクターを入れた。抽象 UI を見てみよう:

package com.eed3si9n.tetrix

class AbstractUI {
  // skipping imports...
  implicit val timeout = Timeout(1 second)

  private[this] val initialState = Stage.newState(Block((0, 0), TKind) :: Nil,
    randomStream(new util.Random))
  private[this] val system = ActorSystem("TetrixSystem")
  private[this] val playerActor = system.actorOf(Props(new StageActor(
    initialState)), name = "playerActor")
  private[this] val timer = system.scheduler.schedule(
    0 millisecond, 1000 millisecond, playerActor, Tick)
  private[this] def randomStream(random: util.Random): Stream[PieceKind] =
    PieceKind(random.nextInt % 7) #:: randomStream(random)

  def left()  { playerActor ! MoveLeft }
  def right() { playerActor ! MoveRight }
  def up()    { playerActor ! RotateCW }
  def down()  { playerActor ! Tick }
  def space() { playerActor ! Drop }
  def view: GameView =
    Await.result((playerActor ? View).mapTo[GameView], timeout.duration)
}

ロックしすぎ 

振り返ってみると上の実装は良いものとは言えない。プログラムが「恥ずかしいほど直列」なものになってしまっているからだ。以前の synchronized を使った実装では swing UI がビューを一秒間に 10回問い合わせることができた。この実装でも同じことが行われているけど、他のメッセージと同じメールボックスに入ったため、一つでも演算が 100 ミリ秒以上かかるとメールボックスが溢れてしまう可能性がある。

理想的にはゲームの状態は、新しい状態が上書きされるその瞬間まではロックをかけるべきではない。プレーヤーによる操作とスケジュールされた時計は常に非互換であるので、今のところはこれらを一つづつ処理するのは理にかなっていると思う。

2つ目のアクターのメッセージ型を定義しよう:

sealed trait StateMessage
case object GetState extends StateMessage
case case SetState(s: GameState) extends StateMessage
case object GetView extends StateMessage

このアクターの実装は単純なものだ:

class StateActor(s0: GameState) extends Actor {
  private[this] var state: GameState = s0
  
  def receive = {
    case GetState    => sender ! state
    case SetState(s) => state = s
    case GetView     => sender ! state.view
  }
}

次に、StageActorStateActor に基いて書き換える必要がある。

class StageActor(stateActor: ActorRef) extends Actor {
  import Stage._

  def receive = {
    case MoveLeft  => updateState {moveLeft}
    case MoveRight => updateState {moveRight}
    case RotateCW  => updateState {rotateCW}
    case Tick      => updateState {tick}
    case Drop      => updateState {drop}
  }

  private[this] def updateState(trans: GameState => GameState) {
    val future = (stateActor ? GetState)(1 second).mapTo[GameState]
    val s1 = Await.result(future, 1 second)
    val s2 = trans(s1)
    stateActor ! SetState(s2)
  }
}

最後に抽象UI を少し変えて stateActor を作成する:

package com.eed3si9n.tetrix

class AbstractUI {
  // skipping imports...
  implicit val timeout = Timeout(100 millisecond)
  
  private[this] val initialState = Stage.newState(Block((0, 0), TKind) :: Nil,
    (10, 23), randomStream(new scala.util.Random))
  private[this] val system = ActorSystem("TetrixSystem")
  private[this] val stateActor = system.actorOf(Props(new StateActor(
    initialState)), name = "stateActor")
  private[this] val playerActor = system.actorOf(Props(new StageActor(
    stateActor)), name = "playerActor")
  private[this] val timer = system.scheduler.schedule(
    0 millisecond, 700 millisecond, playerActor, Tick)
  private[this] def randomStream(random: scala.util.Random): Stream[PieceKind] =
    PieceKind(random.nextInt % 7) #:: randomStream(random)

  def left()  { playerActor ! MoveLeft }
  def right() { playerActor ! MoveRight }
  def up()    { playerActor ! RotateCW }
  def down()  { playerActor ! Tick }
  def space() { playerActor ! Drop }
  def view: GameView =
    Await.result((stateActor ? GetView).mapTo[GameView], timeout.duration)
}

タイマーの時計とプレーヤーのによるピースの移動の並行処理は引き続き playerActor によって保護される。しかし、これで swing UI は他の処理を待たずに好きなだけビューを読み込めるようになった。