LiveViewを使って簡単にステートフルなタイピングゲームアプリを作ろう!前編
この記事は、「Elixir Advent Calendar 2021」の15日目になります。
昨日は、@takamizawa46さんの「多分、UNIXの本読んで出てきた「小さく作る」がElixirでの開発だったこと」でした。
東京だけど fukuoka.ex の YOSUKENAKAO.me です。
The Waggleという会社でScrumとElixirと研修講師をやってます。
Scrum開発で学ぶElixir研修を2020年にテストケースとしてローンチし、9名の方がわずか1ヶ月で
Elixirでモジュール開発から、簡単な地図アプリケーションをデプロイするまで成長しました。
企業向けの研修ですが、個人でも受けたいという方がいらしたらご連絡お待ちしてます。
もちろん、企業様のお問い合わせもお待ちしてます。
ご要望が多ければ個人向けも提供したいと考えております。
なお、研修講師になりたい人も 絶賛募集中です。
こちらは、Elixirだけでなく、JavaやPHP、C、C#, Python、機械学習、統計などの分野でも募集しています。自身のスキルの棚卸しや、コーチングやファシリテーションのスキルなども身につきますので、自身のスキルアップとしてチャレンジしたい方もいましたらこちらのページの下記にあるお問い合わせフォームまでご連絡ください
この記事では、LiveViewを使って簡単にタイピングゲーム擬きを作成していきます。
Phoenixfremorkを既に何回か使ったことある方は本編から読み進めてください。
こちらは2部構成で今回は簡単にタイピングゲームの基礎となるキー入力を受け付けて
消すところまでを実装します。
後半は、「fukuoka.ex Elixir/Phoenix Advent Calendar 2021」の18日目で掲載予定です。
記者の環境とツール
この記事を作成する際に利用している環境はインテルCPUのmacOS Big Sur
利用しているコードは、Visual Studio Code.
バージョン管理ツール asdf
Elixir 1.12.3
OTP-24
Phoenixframework 1.6
環境構築がまだの方
環境構築がまだの方はasdfで環境構築はこちら(macOS)
を参考にasdfの環境を作ります。
バージョンは、Elixir1.12.3 OTP-24をインストール終えたら、
mix local.hex
mix archive.install hex phx_new
をインストールして、PhoenixFrameworkを利用できるようにしてください。
postgresqlもインストールください。
Phoenixframework初心者の方はこちらから
mix phx.new typing_game --live
コマンドを実行すると以下のように次やるコマンドリストが表示されます。
上から順に進めていきます。
Fetch and install dependencies? [Yn] Y
* running mix deps.get
We are almost there! The following steps are missing:
$ cd typing_game
Then configure your database in config/dev.exs and run:
$ mix ecto.create
Start your Phoenix app with:
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
mix phx.server
を実行し、ブラウザでlocalhost:4000
を確認すると、以下のような画面が表示されています。
それでは、これをベースにLiveViewでタイピングしたキー入力を取得してそれに応じてアクションを起こすLiveViewを実装していきましょう。
本編はここから(LiveViewでタイピングゲーム作り)
まずは、libフォルダまで移動しましょう。
lib.
├── typing_game
├── typing_game.ex
├── typing_game_web
└── typing_game_web.ex
libフォルダの中のtyping_game_web
の中にliveView用のページを作成するフォルダを追加していきます。
そして、liveフォルダの中にpage_live.ex
を追加します。
typing_game_web.
├── controllers
│ └── page_controller.ex
├── endpoint.ex
├── gettext.ex
├── live. <- 追加
│ └── page_live.ex <- 追加
├── router.ex
├── telemetry.ex
├── templates
│ ├── layout
│ │ ├── app.html.heex
│ │ ├── live.html.heex
│ │ └── root.html.heex
│ └── page
│ └── index.html.heex
└── views
├── error_helpers.ex
├── error_view.ex
├── layout_view.ex
└── page_view.ex
page_live.ex
の中には、live_viewを使う為のモジュールの宣言をしていきます。
defmodule TypingGameWeb.PageLive do
use TypingGameWeb, :live_view
end
続いて、mountを追加します。
mount関数には、paramsやsessionを受け取り口がありますが、
今回はsocket以外は使わないので_をつけています。
socketで受け取る引数に resultsとvalの2つを準備しておきます。
valには、まずliveViweの特徴を理解する為の簡単なレクチャー用なので、
一旦気にせずに準備してください。
defmodule TypingGameWeb.PageLive do
use TypingGameWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, results: %{}, val: 0 )}
end
end
それでは、liveViewの理解を進めるために一旦、一時的にrender関数を用いて、
ページ表示を行う機能を追加していきたいと思います。liveViewのrender関数
について既に知っている方は飛ばして、タイピング実装まで進んでください。
defmodule TypingGameWeb.PageLive do
use TypingGameWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, results: %{}, val: 0 )}
end
def render(assigns) do
~L"""
<div>
<h1>The count is: <%= @val %></h1>
<button phx-click="dec">-</button>
<button phx-click="inc">+</button>
</div>
"""
end
end
~LはLiveViewのシジルです。
さて、これで、LiveViewのページをレンダリングする関数を追加したので、
router.exのscopeにルートを追加していきます。
scope "/", TypingGameWeb do
pipe_through :browser
get "/", PageController, :index
live "/count", PageLive, :index. #<- 追加
end
サーバを起動して、localhost:4000/count
にアクセスしてみましょう。
それでは、続いて、redner関数で作成したページのボタンをクリックして、カウントを更新する処理をLiveViewで実装していきます。
render関数内に書かれているbuttonタグの中にphx-click
があります。ここに”dec”や”inc”というキーが渡されているので、このボタンが押された時に反応するeventを受け付ける関数を実装します。
できたら、ボタンを押して変化する事を確認します。
defmodule TypingGameWeb.PageLive do
use TypingGameWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, results: %{}, val: 0 )}
end
def render(assigns) do
~L"""
<div>
<h1>The count is: <%= @val %></h1>
<button phx-click="dec">-</button>
<button phx-click="inc">+</button>
</div>
"""
end
def handle_event("inc", _, socket) do
{:noreply, assign(socket, val: 1)}
end
end
これで、+
ボタンをクリックすると、カウントが1に更新されます。
しかし、このままだと何度押しても、1が更新されるだけなので、カウントアップ
するように記述を変えていきます。
&演算子を利用して、+1する関数を作ります。
またこの時、update関数にて実装します。
def handle_event("inc", _, socket) do
{:noreply, update(socket, :val, &(&1 + 1))}
end
これを真似して、dec
の方も更新する処理を書いてみてください。
続いて、render関数で実行していたものを移植していきます。
移植したら、render関数は削除しましょう。
live/page_live.html.heex
を作成し、render関数に記述した内容を移植していきます。
なお、ちょっとオシャレにしたいので、<section class="phx-hero">
タグを追加してみました。
<section class="phx-hero">
<h1>The count is: <%= @val %></h1>
<button phx-click="dec">-</button>
<button phx-click="inc">+</button>
</section>
localhost:4000/count
にアクセスすると以下のように表示され、カウントも追加できます。
それでは、準備ができたところで、前編のクライマックスとして、キー入力を取得して表示するところまで実装していきます。
LiveViewでキーを受け付けて表示する
まずは、page_live.ex
の中身から書き換えていきます。今回は、mount関数で、初期表示するデータを扱うword
を用意しておきます。
次に、handle_event
を作成します。この関数では、typing
で取得した key
を受け付けて、word
の値に受け付けたkey
を渡して更新します。
defmodule TypingGameWeb.PageLive do
use TypingGameWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, word: "test case" )}
end
@impl true
def handle_event("typing", %{"key" => key}, socket) do
{:noreply, assign(socket, word: key) }
end
end
続いて、page_live.html.heex
の中を整えていきます。
keyイベントを受け付ける為のコンテナを追加し、typingというキーワードを準備します。
そして、@word
を置くだけです。これで、実装は完了です。あとは、キーを叩いて、
叩いたキーが表示されることを確認しましょう。
<section class="phx-hero">
<div class="container"
phx-window-keyup="typing"
>
<p><%= @word %></p>
</div>
</section>
さて、準備が整ったところで、タイピングゲームのように表示されているワードから
入力したキーを判定するところまでを作成していきましょう。
まずは、word の先頭文字と押したkeyがあっているかの判定をしたいので、wordの頭文字を
keyupした際にsocketに値として渡すようにしたいと思います。
また、wordも先頭の文字がヒットした時に 文字を削りたいので、更新して行けるように値を返すことを想定して、keyupの際にsocketに引き渡します。この時、keyはどこで入力しても、受け渡したい値は<div class="container">
のDOMに対して実施してほしいので、focusとblurイベントも実装しておきます。
<section class="phx-hero">
<div class="container"
phx-window-focus="page-active"
phx-window-blur="page-inactive"
phx-window-keyup="typing"
phx-value-word={ @word }
phx-value-char={ String.at(@word, 0) }
>
<p><%= @word %></p>
</div>
</section>
handle_event
では、keyとcharとwordを受け取り、keyとcharが同じなら、wordの頭文字を削ってwordに再束縛する処理を追加しました。
これで、同じ文字なら表示されている文字が削れていく表現が実現できました。
@impl true
def handle_event("typing", %{"key" => key, "word" => word, "char" => char}, socket) do
word = if char == key do
[_head | tail] = String.graphemes(word)
List.to_string(tail)
end
{:noreply, assign(socket, word: word) }
end
以上で、タイピングゲームを作成する為の最低限の土台ができたかと思います。
後半では、タイピングの文字が複数リストから出題など追加して、よりゲームっぽい表現を実装していきたいと思います。
この記事が面白いと思った方はぜひ、いいね、お願いします。
モチベーションにつながります。