👾

Scala.jsでテトリスを作ってAWS Amplifyでホスティングしてみる

2021/04/18に公開

はじめに

あのプログラミング言語だったらこんなことするのラクなのに〜 を 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 です。

project/plugins.sbt
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.5.1")

plugin を有効化します。

build.sbt
enablePlugins(ScalaJSPlugin)

Canvas API を呼びたいので、Scala.js から dom を扱えるライブラリを追加します。

build.sbt
libraryDependencies ++= Seq(
  "org.scala-js" %%% "scalajs-dom" % "1.1.0"
)

scalajs-bundler のセットアップ

参考: scalajs-react-tutorial

scalajs-bundler は、npm package を扱えるようにする sbt plugin です。
今回のゲームでは、index.html や css もいい感じにバンドルしたいので使用しています。内部で npm と webpack が使用されているようです。
upickleは JSON を変換するライブラリで、package.json を読み込んで scalajs-bundler の設定に変換するために使用しています。

project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.20.0")

libraryDependencies ++= Seq(
  "com.lihaoyi" %% "upickle" % "1.2.2"
)

plugin を有効化します。

build.sbt
enablePlugins(ScalaJSBundlerPlugin)

次のコードで project root にあるpackage.jsonを変換します。

project/PackageJson.scala
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-react-tutorial

次の設定はscalajs-bundlerに関するもので、主に webpack の設定を行っています。

build.sbt
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.jsopt.main()fastOpt.main()が紐付いています。

src/main/scala/tetris/App.scala
  @JSExportTopLevel("main")
  def main(): Unit = {
    ???
  }

ローカルで動かしてみる

次のコマンドで起動します。http://localhost:9000にアクセスするとゲーム画面が表示されるはずです。
コードの変更を検知して、自動ビルドと画面のリフレッシュを行ってくれます。

sbt dev

Amplify でホスティング

参考: scalajs-react-tutorial

先にビルドして dist フォルダに成果物を出力させましょう。

sbt dist

Amplify 側のセットアップは 公式ドキュメント で割愛させていただきます。

1 点注意しないといけないのは、Amplify の CI/CD 機能を使うときはビルドイメージに sbt が入ってないと失敗することです。
すでにビルド済みのイメージは Docker Hub に置いています。実際の中身は Dockerfile にあります。

Amplify Console
Build settings > Build image settings からshomatan/amplify-javaを指定

おわりに

Scala でも簡単なゲームを作ることができました!今回は諦めましたが、対戦ゲームとかにも挑戦してみたいです。
ゲームだとミュータブルなデータ構造のほうが扱いやすいと思うのですが、ミュータブルなところを局所化してできるだけイミュータブルで書くようにしたのが脳トレになって面白かったです。

この記事を見て Scala 始めてみよっかなって思ってもらえると嬉しいです!

参考

Discussion