昨日はゲームの状態への並行処理を管理するために 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
}
}
次に、StageActor
を StateActor
に基いて書き換える必要がある。
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 は他の処理を待たずに好きなだけビューを読み込めるようになった。