Scala.jsでテトリスを作ってAWS Amplifyでホスティングしてみる
はじめに
あのプログラミング言語だったらこんなことするのラクなのに〜 を Scala でやってみる第二弾です。
Scala やっててこんなこと思ったりしませんか?
- ゲーム作ってみたいけど Scala じゃ大変だよね?
それいい感じにできるソリューションあります!
サンプルの完成品はコチラ ignission/scalajs-amplify-tetris からご覧いただけます。
実際に遊んでみたい方は コチラ から遊ぶことができます。
ゲームはテトリスを題材にしています。ロジックは lihaoyi/scala-js-games を参考にさせていただいてます。
その他シリーズの記事はコチラ:
要約
- Scala.js は Scala を JavaScript に変換してブラウザで動かすことができる
- scala-js-dom から Canvas API を呼んでグラフィックを描画する
- scalajs-bundler で npm package もバンドルできる(内部で npm と webpack を使用している)
- WebSocket は今回やらず、シングルプレイのみ(遊びすぎ注意 ⚠️)
- Scala.js プロジェクトを AWS Amplify でホスティングして CI/CD するときは、sbt が入った docker image を public なところに置いておかないといけない
気持ちいい!
説明
プロジェクトのセットアップ
Scala.js
Scala.js を使用するには、sbt plugin を追加するだけで OK です。
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.5.1")
plugin を有効化します。
enablePlugins(ScalaJSPlugin)
Canvas API を呼びたいので、Scala.js から dom を扱えるライブラリを追加します。
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "1.1.0"
)
scalajs-bundler のセットアップ
scalajs-bundler は、npm package を扱えるようにする sbt plugin です。
今回のゲームでは、index.html や css もいい感じにバンドルしたいので使用しています。内部で npm と webpack が使用されているようです。
upickle
は JSON を変換するライブラリで、package.json を読み込んで scalajs-bundler の設定に変換するために使用しています。
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.20.0")
libraryDependencies ++= Seq(
"com.lihaoyi" %% "upickle" % "1.2.2"
)
plugin を有効化します。
enablePlugins(ScalaJSBundlerPlugin)
次のコードで project root にあるpackage.json
を変換します。
import upickle.default._
case class PackageJson(
dependencies: Seq[(String, String)],
devDependencies: Seq[(String, String)]
)
object PackageJson {
implicit val r: Reader[PackageJson] = JsObjR.map { obj =>
PackageJson(
dependencies = readDeps(obj, "dependencies"),
devDependencies = readDeps(obj, "devDependencies")
)
}
private def readDeps(obj: ujson.Obj, key: String) =
obj(key).obj.map { case (k, v) => k -> v.str }.toSeq
def readFrom(readable: ujson.Readable): PackageJson =
read[PackageJson](readable)
}
webpack の設定
次の設定はscalajs-bundler
に関するもので、主に webpack の設定を行っています。
lazy val packageJson = settingKey[PackageJson]("package.json")
useYarn := true
webpack / version := "4.46.0"
startWebpackDevServer / version := "3.11.2"
webpackResources := baseDirectory.value / "webpack" * "*"
packageJson := PackageJson.readFrom(baseDirectory.value / "package.json")
Compile / npmDependencies ++= packageJson.value.dependencies
Compile / npmDevDependencies ++= packageJson.value.devDependencies
fastOptJS / webpackConfigFile := Some(
baseDirectory.value / "webpack" / "webpack-fast.config.js"
)
fullOptJS / webpackConfigFile := Some(
baseDirectory.value / "webpack" / "webpack-full.config.js"
)
fastOptJS / webpackDevServerExtraArgs := Seq("--inline")
fastOptJS / webpackBundlingMode := BundlingMode.LibraryOnly()
Test / requireJsDomEnv := true
webpack 正直良くわかってないですが、生の webpack の設定をしないといけないのでとりあえず webpack ディレクトリ のように追加してみます...
Entry point の設定
設定が終わったらさっそくゲームを作っていきましょう!
JSExportTopLevel
アノテーションをつけることで、JavaScript 側からも呼べるようになります。
webpack/scalajs-entry.js のopt.main()
とfastOpt.main()
が紐付いています。
@JSExportTopLevel("main")
def main(): Unit = {
???
}
ローカルで動かしてみる
次のコマンドで起動します。http://localhost:9000
にアクセスするとゲーム画面が表示されるはずです。
コードの変更を検知して、自動ビルドと画面のリフレッシュを行ってくれます。
sbt dev
Amplify でホスティング
先にビルドして dist フォルダに成果物を出力させましょう。
sbt dist
Amplify 側のセットアップは 公式ドキュメント で割愛させていただきます。
1 点注意しないといけないのは、Amplify の CI/CD 機能を使うときはビルドイメージに sbt が入ってないと失敗することです。
すでにビルド済みのイメージは Docker Hub に置いています。実際の中身は Dockerfile にあります。
Build settings > Build image settings からshomatan/amplify-java
を指定
おわりに
Scala でも簡単なゲームを作ることができました!今回は諦めましたが、対戦ゲームとかにも挑戦してみたいです。
ゲームだとミュータブルなデータ構造のほうが扱いやすいと思うのですが、ミュータブルなところを局所化してできるだけイミュータブルで書くようにしたのが脳トレになって面白かったです。
この記事を見て Scala 始めてみよっかなって思ってもらえると嬉しいです!
Discussion