( )

ハッカソンイベントで、React Konva製のジグソーパズルっぽいパズルを作ってみた

イベント

JavaScript, React, web1week


個人開発って何作るか悩んだり、モチベを保つのが難しかったりしますよね。
そんな自分が、先日 web1week というイベントで個人開発にチャレンジしました。
何とかリリースまでできたので、使用した技術やどんなことをやったのかといった内容を残しておきます。

はじめに#

web1week って?#

簡単に言うと「1週間でお題に沿ったWebサービスを作ってみよう」というイベントです。
プログラマー、クリエイターが記事投稿や議論ができるコミュニティ、Crieit の運営者である、だらさん主催で行われました。

※2024/09/14追記:Crieit はサービスクローズされました。

参加しようと思ったきっかけ#

主な背景としては、こんな感じ.

  • 個人開発に使える時間があった
  • 前回の開催時も面白そうと気になっていた(今回は2回目の開催)
  • 個人開発をやってみたかった(+こういったイベントならモチベも保てるかなと思った)

個人開発は以前からやりたいと考えることはありつつ、結局モチベが続かなくて止めてしまったりということが多かったのです…。
もし何か作ることができたら、1つの実績にできて自分の財産になるかなーと思いました。

これは…やるしかない。

ということで参加。

自分が作ったもの#

ちょっとしたパズルを作りました。
PC向け。スマホ対応は現状してません。

パズルのプレイ画面GIF

なんでパズル?#

イベントボードへの投稿にも書いていたので引用します。

「Like」ということで
好きなもの → 趣味とかかな? → 絵を描くこと(最近全然描いてないけど) → Canvasでお絵描き実装ができるらしい → でも、ただ絵を描くだけじゃつまらない → もしかしてジグソーパズル作れそう? といった感じで行きつきました。

ジグソーパズル特有の形は再現できてないので、あくまで「ジグソーパズルっぽいもの」ですね。
完全に後付け理由ですが、Like って「~ようなもの」って意味もありますし、意図せずテーマに沿ったものになりました(笑)
こんなことあるんですねー。

構成#

構成図描こうかなとも思ったんですが、大した構成でもないのでざっくり文面で書きます。

開発は Docker の Node.js コンテナで。
本番は Firebase Hosting でホスティングしています。

使用した主なライブラリはこちら(正確には他にもあります)

  • React:言わずと知れた UI 構築ライブラリ(create-react-app で導入)
  • React Konva:Canvas を扱うライブラリである Konva.js の React 版
  • React Router:ルーティング
  • Material UI:UI コンポーネント集
  • PropTypes:props のバリデーション
  • ESLint:静的解析
  • Prettier:コードフォーマッター

基本的な UI は Material UI で構築しました。
Box コンポーネントがすごい便利でしたね。おかげであまり CSS を書かずにすみました。div がその分増えましたが。
パズル部分は React Konva で構築しています。

React Konva と Firebase については初めて使ったので、まだまだちゃんとわかってないことも多いです。

どんな感じで開発してたのか#

ざっくりこんな感じでした。

1日目:5/18(月)#

お題に沿って何を作るか案がなかなか出てこなかったです。
React Konva に行きついてからドキュメントをひたすら読んで、夕方くらいにやっとパズルいけるかも?ってなりました。

2日目:5/19(火)#

午後からやっと手元の環境で動かしてみました。
色々試してみて、なんとなくいけそうかな?という手ごたえがありました。

ただ、そこからパズルの元となる画像の適切なサイズ割り出しに時間かかりました…。
3パターンできれいに割れて、適度な大きさのサイズがいいよねってなりまして。

3日目:5/20(水)#

ようやく画像サイズを決定。

720 * 480

  • 初級:120 * 120 → 6 * 4 = 24
  • 中級:80 * 80 → 9 * 6 = 54
  • 上級:60 * 60 → 12 * 8 = 96

その後、ストップウォッチの実装をどうやるかめっちゃ悩みました…。
記事を参考にしながら試すも、うまくいかずドはまり。
時間かかりつつも一応実装できました。

この時点でいまだにリポジトリを作っていなかったので、とりあえず作成だけ。

4日目:5/21(木)#

ルーティングや OGP、画像の取り扱いをどうしようか悩みました。
結果的には、最低限遊べるレベルのリリースができればいいやということで、一旦は妥協することにしました。

午後からやっとプロジェクトのセットアップ。
Issue やプルリクのテンプレ、Docker で開発環境構築ともろもろ必要な準備を整えました。

ピース位置チェック(正解位置に置かれたらはまる)のやり方がなんとなくわかって、よーし作っていくぞーという流れへ。

5日目:5/22(金)#

パズル画面に背景テクスチャをいれようとしましたが、迷ったので結局止めました。

黙々と進めて、ぼんやりとしたイメージで作っていった画面のモックがおおよそできました。

モックに続いて、難易度選択モーダル、ピース生成ロジックを作成。

6日目:5/23(土)#

ポーズモーダル、クリアモーダルを作成。

ここまでで、とりあえず最低限遊べることは確認できたのでリリースすることに。
コンポーネントを多少分けてはいましたが、ファイル分割とか全然できておらず。
そこまでリファクタやってからリリースするかとも考えたのですが、もうやっちゃえとなりました。

ホスティングに関しては、多少慣れてる Netlify でやる手もあるなと思いました。
ただ、今後機能拡張していくとしたら Firebase の方が色々やれてよさそうと思い、こちらですることにしました。

リリース(v0.1.0)して投稿。
リリースと言いつつ、色々と足りてないものがまだあるのでプレリリースみたいなものですね。

おおよそどんなことをやっているのか#

ソースコードはリファクタやったりして変わる可能性があるので、リポジトリ見ていただいた方よさそう(あまりきれいなコードではないですが…)
一応、Issue 書いたりしながら進めました。

以下の内容は執筆時点(v0.1.4)での実装のものとなります。
有識者の方からすると、この実装イケてないとかあるでしょうがご容赦ください。

ストップウォッチ#

パズル画面のストップウォッチ部分画像

概要#

setInterval()clearInterval()を使って実装。
恥ずかしながら自分はこの関数を使用したことがなかったこともあり、最初はストップウォッチってどうやって実装したらいいんだろう?という状態でした。

実装のやり方についてはこちらの記事をとても参考にさせていただきました。

1秒ごとに秒数カウントを+1して、その秒数カウントをもとに時、分、秒を計算して更新していくというものです。

問題点#

同画面で開始と停止をする上では問題なかったのですが、今回の場合は以下のような仕様でした.

  • パズル画面の「一時停止」ボタンを押す → ストップウォッチを停止してポーズモーダルを開く
  • ポーズモーダルの「復帰」ボタンを押す → ポーズモーダルを閉じて、ストップウォッチ再開

そのため、再開時に時間が最初からになってしまう問題が起きました。

秒数カウントはstateで管理してない変数だったので、再レンダリング時に値がリセットされていたんだろうなと。
そのため、秒数カウントの値をバックアップを取っておく感じで state でも保持するようにしました。
1秒ごとの更新処理の際に、秒数カウント(変数)が0 だったら state を確認して、バックアップがあればそこから復元するイメージです。

秒数カウントを最初から state で管理すればいいのでは?と思われそうですが、state でやるとうまく動いてくれなかったので、こういった形をとりました。

パズル#

パズル画面のパズル部分画像

概要#

React Konv aで実装していますが、その実体は Canvas です。
Canvas を扱うKonva.jsというライブラリがあり、その React 版だそうです。

Canvas を扱うにはKonva.jsが便利らしいみたいな記事は複数見かけたんですが、自分は Canvas 自体を使ったことがなかったため、いまいちピンとこず…。
なので、最初はひたすらドキュメントを読んで、おおよそどんなことができるものなのかを見ていきました。
その結果、パズルいけそうだなという目途がついたので使ってみたという背景があります。

Konva.js の構造としては、以下のようになっています

Konva.jsの構造画像

Shapeは複数種類があり.

  • Rect(長方形)
  • Circle(円)
  • Ellipse(楕円)
  • Line(線)
  • Image(画像)
  • Text(テキスト)
  • Star(星)

などがあてはまります。

これらは上位の要素を基準とした x、y座標であったり、横幅、縦幅、色、影などを指定することで、Canvasに描画をしていけるようになっています。

パズルの元画像#

Image コンポーネントを使用。
普通に画像パスを渡すのではダメらしく、use-imageライブラリのuseImageフックを使って生成した、DOM 画像を渡すようにしています。

ちなみにピース数の計算との兼ね合いで、画像サイズおよびこの Image コンポーネントのサイズは 720 * 480 で固定しています。

パズルの額縁#

Line コンポーネントを使用。
4つの点の座標を指定して繋ぐことで図形を描画。これを上下左右で4つ作成しています。
ただの塗りつぶしだと安っぽくなるので、グラデーション指定にしてみました。

パズルのピース#

Image コンポーネントを使用。
useImageフックによる DOM 画像を渡しているのは同様ですが、crop を指定することで画像の切り抜きをしています。

例として初級の場合であれば、ピースサイズは 120 * 120 なのでこんな感じです。

1行目
- {x:0 y:0 width:120 height:120}
- {x:120 y:0 width:120 height:120}
- {x:240 y:0 width:120 height:120}
.
.
.

2行目
- {x:0 y:120 width:120 height:120}
- {x:120 y:120 width:120 height:120}
- {x:240 y:120 width:120 height:120}
.
.
.

合わせてコンポーネント自体のサイズも 120 * 120 を指定になります。

draggable を有効化してドラッグアンドドロップができるように。
そのうえ、onDragStart と onDragEnd でイベント処理を実装しています。

ピースドラッグ時の挙動#

scale を変えて、少しだけピースが大きくなるようになっています。
(公式デモのコードそのまま持ってきた感じです)

それに加えて、ドラッグしているピースが必ず最前面に来るような処理をしています。
Canvas 要素は、あとに定義したものが前面へ来るようになっているようです。
そのため、この処理をやらないと場合によっては、はめ込まれたピースの背面にドラッグ中のピースが隠れてしまい操作不能になってしまうことがあります。
そんなことなったら一気に萎えちゃいますよね。

ピースドロップ時の挙動#

scale を元に戻します。duration も設定してるので、ポヨンと大きさが戻るような見た目になってます。
(これも公式デモのコードをそのまま持ってきた感じです)

加えて、ドロップされた座標と正解位置の座標を比較。
誤差の範囲内であれば、draggable を無効 + ピースの座標を正解位置の座標に更新 することで、ピースがはめこまれるような挙動を実現しています。
この処理はこちらの公式デモを参考にしました。

ゲームの流れ#

これまでの内容を踏まえて、ゲームの流れとしてはおおまかにこんな感じです。
(並列で処理しているところもあります)

難易度選択

難易度に応じたピース数(縦、横)、ピースサイズの値をセット

この3つの値の変更を検知して、初期化ロジック実行
ピースの情報を持ったオブジェクトの配列を生成後、その順番をシャッフル

ピースの情報を持ったオブジェクトの配列をもとにピースのコンポーネントが描画される

ゲーム開始(ストップウォッチ開始)

ピースのドラッグアンドドロップ
ドロップ座標が正解位置の座標の誤差範囲であればはめこまれ、正解ピース数が+1される
(これを全てのピースがはめ込まれるまで繰り返す)

正解ピース数の値の変更を検知して、総ピース数と一致すればクリア(ストップウォッチ停止)

今回参加してみてどうだったか#

楽しかったです!ただ、疲れました(笑)
お題があったとはいえ、ほぼ一から自分で考えて作る必要があったので、普段よりもいっぱい頭使ったからかなと。

とはいえ、個人開発として無事にリリースまでできたのはこれが初めてなので素直に嬉しいですね。
他の方の投稿を見るのも楽しいですし、学ばせていただく機会にもなりました。
こういった機会を設けてくださり、ありがとうございました!


ちょっとしたレポート記事を書くつもりがすっかり長くなってしまいました…。
ここまで読んでくださった方、ありがとうございます!

パズルの方は今後も合間を見つけて改修していこうかなと思ってます。
ちなみに最初に導入した Google Analytics がちゃんと動いておらず、投稿時のアクセス数を見られなかったというヘマをやらかしていましたが、修正して現在は無事に動いてます(冷や汗)

もしお暇な時があれば、パズル部屋を覗いてみてください。

参考リンクまとめ#