Higman 備忘録

覚え書きなどなど

ScalaFXでランダムウォーク

これは香川大学工学部サークルSLPのアドベントカレンダー16日目の記事です。 adventar.org

概要

簡潔にいうと「以前、Swing(Java)で書いたランダムウォークのプログラムをScalaFxで書き直そう」っていう思いつきの企画。 動機としては、Swingで書いたプログラムがJava特有の長いコードと機能の後から付け足しが原因の複雑化により、作成から数カ月たった今では、読んでも???な感じであったため、Scalaの勉強ついでに作り直そうと思ったから。

今回使った言語は、ScalaGUIライブラリはScalaFX。 ScalaFXってのは、名前から分かるかもしれませんが、JavaFXScalaで使いやすいようにしたライブラリで、 JavaFXと同様にFXMLを用いてレイアウト構築もできる便利なやつです。

目的の確認

以前開発したランダムウォークのアプリケーションは、こんな感じ。

f:id:Higman:20171216002243g:plain:w300
開始位置や移動速度などの操作部が左側に用意されています。 しかし、今回は、右側のランダムウォークの実行部と実行ボタンのみの簡単なものを作成します。

ランダムウォークって?

冒頭から言っているランダムウォークってのが、何なのか簡単に説明すると...

ランダムウォーク(英語: random walk)は、次に現れる位置が確率的に無作為(ランダム)に決定される運動である。日本語の別名は乱歩(らんぽ)、酔歩(すいほ)である。グラフなどで視覚的に測定することで観測可能な現象で、このとき運動の様子は一見して不規則なものになる。 (Wikipedia: https://ja.wikipedia.org/wiki/ランダムウォーク)

...らしいです。

今回のランダムウォークは、碁盤目状の盤面上を複数の物体をランダムに指定回数移動させるもので、物体がいる位置の盤面の形状によって、確率が変動するというものです。

ランダムウォークの規則

盤面の形状と各方向に進む確率の関係は、以下のようになります。

f:id:Higman:20171215234230j:plain

盤面は碁盤目状になっているので、┼、┬、┐の3つの形状のマスが存在し、 物体の進行方向に応じて、各方向へ進む確率が割り当てられています。 ただし、開始地点からの移動時には、全ての方向への移動確率は同じとします。 たとえば、十字が開始地点であった場合、4つの進行方向の全ての確率は1 / 4となります。

今回の開発環境

  • Windows10
  • Scala (version: 2.12.4)
  • ScalaFX (version: 8.0.144-R12)
  • Java (version: 1.8.0_151)
  • sbt (version: 1.0.4)
  • IntelliJ IDEA 2017.3.1 (Ultimate Edition) (Build #IU-173.3942.27)
  • JavaFX Scene Builder 8.4.0

さっそく開発

コードの量がそこそこある(もちろん、Swingよりは少ないが)ため、コードは、部分的な紹介にとどめておきます。 後日、コードをGitHubにあげておくので、そちらを参照してほしいです。 GitHubに今回作成したランダムウォークのコードをアップしておきました。 詳細はこちらを参照してください。

github.com

レイアウトの作成

まずは、ウィンドウを表示させる。 手順は、JavaFXと同じ、今回はFXMLを用いた方法で行います。

qiita.com ググったら、まぁまぁでてくる。 qiita.com 今回は、上記の記事で紹介されているJavaのコードを参考にしました。 上記の記事では、JavaFXのControllerクラスのクラスフィールドとメソッドとして、 FXMLのレイアウトの読み込みを行うことで、シーンの切り替えを容易にしようとしています。 もちろん、ScalaFXでも、ほぼ同じように実装することができます。 Scalaでは、クラスフィールドやメソッドを定義するのに、Javaの様なstaticが用意されていないので、 objectクラスを用いることになります。 FXMLの読み込み部分をScalaのコードにしたものを以下に示します。

import java.io.IOException
import javafx.fxml.FXMLLoader
import javafx.scene.Parent

import scalafx.Includes._
import scalafx.scene.Scene

/**
  * コントラーラクラスのオブジェクトクラス
  */
object RandomWalkController {
  // FXMLファイルの場所
  private val FXMLFileName = "fxml/random_walk_controller.fxml"  
  // FXMLLoader
  private val RandomWalkerFXMLLoader = getLoader      
  
  // 指定のFXMLファイルのシーン
  val Scene = new Scene(RandomWalkerFXMLLoader.getRoot[Parent])  
  // RandomWalkControllerのインスタンス
  val Instance: RandomWalkController = RandomWalkerFXMLLoader.getController[RandomWalkController]

  /**
    * FXMLLoaderの取得
    * @return
    */
  private def getLoader: FXMLLoader = {
    val fLoader: FXMLLoader = new FXMLLoader(ClassLoader.getSystemResource(FXMLFileName))

    try {
      fLoader.load()
    } catch {
      case e: IOException => e.printStackTrace
    }

    fLoader
  }
}

~以下省略~

ここで注意するのが、import scalafx.Includes._です。 ScalaFXのドキュメント を確認すれば分かると思いますが、 scalafx.Includesトレイトには、JavaFXで利用されるJavaFX関連のクラス型を、 対応するScalaFX関連のクラス型へ暗黙的に変換してくれる関数群(implicit修飾子がついている関数)が用意されています。 このimport文がなかった場合、val Scene = new Scene(RandomWalkerFXMLLoader.getRoot[Parent]) などでエラーが発生します。 この暗黙の型変換もScalaの魅力の1つといえます。

このコントローラクラスに、UIのイベントメソッドなどを記述していき、 Scene Builderを用いて、それっぽい感じにレイアウトしたときのアプリケーションがこちら。

f:id:Higman:20171215205304p:plain:w300
わーい!!

ランダムウォーク実行部の作成

実行部で用いられる主なクラスは、TileクラスとRandomUnitクラスです。 前者は、盤面を構成するマス(形状が ┼、┬、┐など)、後者は、ランダムウォークを行う移動物体となります。

物体の移動では、現在いるタイルから隣接している別のタイルに移動する際に、どの隣接タイルへ移動するを 進行方向とタイルの形状により判断するように実装します。 Swing版のプログラムでは、タイルに進行方向の確率を持たせていましたが、 ScalaFX版では、移動物体に確率の決定権を持たせることにしました。この変更により、移動するものが進行方向を決定するという自然な動作になり、 移動物体毎に、異なる確率を持たせることができるようになりました。

画面の左方向に進むように、十字のタイルから別の十字のタイルに移動させた際の 進行方向の選択例を以下に示します。 画像のFRONTWARDは、移動物体から見て、前方に移動したということを表しています。 後方に移動したことを表すBACCKWARDが出現していないが、確率が10%で、他の進行方向よりも小さな確率であるため、10回中1回もないことはありえるといえます。 画像のそれぞれの出現回数も、上記のタイルの形状と確率を表した画像の通りであるといえます。

f:id:Higman:20171215223826p:plain

隣接しないタイルや存在しないタイルに移動しようとした場合、移動できないことを示すNoneが返されます。

f:id:Higman:20171215225013p:plain

いい感じ!! この移動進行方向の決定処理が実装できたら、ほぼ完成したも同然!

アニメーションと開始ボタンの実装

アニメーション

移動物体があるタイルから別のタイルに移動する際に、徐々に移動するように表現したいため、 一定間隔で指定の処理を繰り返すTimelineクラスを利用しました。 ここで重要なのは、javaの標準ライブラリで用意されているThreadクラスやTimerクラスを用いないことです。 JavaFXでは、JavaFXアプリケーション・スレッド以外のスレッドからレイアウトを変更しようとすると例外が発生することが有ります。 それを回避するために、JavaFXアプリケーション・スレッドにUI変更依頼を投げることができるPlatforms.runLaterというメソッドが用意されていますが、 実行タイミングが、場合によっては遅れる場合があり、呼出しすぎると処理が重くなり、予期せぬ動作を引きおこす可能性があります。

Timelineクラスは、定期的に実行する処理を実装でき、JavaFXイベントハンドラ上で実行されるので、直接レイアウトの変更処理を行うことができます。 実際便利。

    //== ランダムウォーク アニメーション処理
    if ( animation != null ) { animation.stop }  // 以前のアニメーションの停止
    animation = new Timeline(new javafx.animation.Timeline(new KeyFrame(Duration.millis(AnimationInterval), (event: ActionEvent) => {
      randomUnits.foreach(unit => unit.move(mapData))  // 全ての移動ユニットの移動
    })))

    animation.setCycleCount(Timeline.Indefinite)  // 処理のループ設定
    animation.play  // 開始

開始ボタン

最後に、アニメーション(ランダムウォーク)の開始ボタンを実装。 これは、JavaFXと同じで、FXMLに指定のボタンを追加し、ボタンを押した時の処理をコントラーラクラスに記述します。

f:id:Higman:20171215232627p:plain

  /**
    * スタートボタンをクリックしたとき
    *
    * @param event
    */
  def OnClickedStart(event: ActionEvent): Unit = {
    startRandomWalk
  }
}

動作確認

では、実際に動かしてみましょう。

f:id:Higman:20171215233721g:plain

黒線上から逸れることなく、ちゃんと物体が移動している。 完成、できたっ!!

最後に

かなり駆け足になりましたが、 ScalaFXを用いたランダムウォークのアプリケーションの紹介でした。 今回、規模の小さな開発、且つ、以前のプログラムの作り直しということも有り、 Scalaの能力を十分に発揮で来ていないと思いますが、 それでも、Javaと比べ、コード量の減少や可読性の向上などが実感できました(書くのがホントに楽でした)。 今回は時間がなかったので最小限の機能しか実装できませんでしたが、 今後、Scala版のランダムウォークアプリケーションにSwing版と同じ機能を実装していこうと思います。

github.com

最後まで読んで頂きありがとうございました。