バインダー開発の裏側: 仕様づくりからCanvas実装まで

こんにちは。リードエンジニアの冬鏡です。HEARTBEATS のプロダクトである ORICAL には、ユーザーが集めたカードや装飾アイテムを自由に配置して、自分だけの画面を作れる「バインダー」という機能があります。

バインダー機能の紹介画面

バインダー機能の利用イメージ。権利保護のため、一部カード画像をぼかしています。

単に「Canvas を使った機能を作った」という話ではありません。なぜ開発者が仕様検討段階から入ったのか。なぜグリッド配置ではなく自由配置にしたのか。Canvas の上で、移動・回転・衝突判定・ピンチ操作をどう扱ったのか。そして、複数パートナーに展開するプロダクトとして何を意識したのか。

普段の HEARTBEATS の開発がどのように進んでいるのかを、ひとつの機能開発の事例として紹介します。

今回は、バインダー機能の開発に携わった加古さんに、仕様づくりから実装、リリース後の横展開までの話を伺いました。

普段の機能開発はどう進むのか

冬鏡: まず、読者向けに普段の機能開発から運用までの流れを軽く説明していただけますか。機能開発は基本的には事業部側から要望が上がってくる感じですよね。

加古: そうですね。基本的には事業部と、仕様を決めるプロダクトチームがいて、そこから仕様書が降ってくることが多いです。ただ、仕様書を決める段階から開発が触ることも結構あります。「この仕様で本当にいいんですか」といった話をプロダクトチームとすることは多いです。

バインダー機能も実際それに近いと思います。大元の発案はプロダクトチームで、そこから仕様を話し合いながら開発が肉付けしていく形でした。

冬鏡: 仕様がある程度決まったら、開発チケットを取って実装に移る。今の我々だと、基本的にはバックエンドが先で、ある程度できたらフロントエンドが入る、という流れですよね。

加古: はい。一般的にはそうです。

資料: 通常の開発フローとバインダー機能の位置づけ

通常の開発フローと、バインダー機能で開発者が早期から関わった位置づけ。

  • 事業部・プロダクトチームが機能の方向性や仕様を検討する。
  • 仕様書が固まった段階で、バックエンド・フロントエンドの実装に入る。
  • ただし、仕様検討段階から開発側が相談に入ることもある。
  • バインダー機能は、開発者が仕様づくりにかなり早い段階から関わったケース。

バインダーは何が特殊だったのか

冬鏡: バインダー機能の場合はどうだったんでしょう。

加古: バインダーはちょっと特殊でした。リリース前にプロトタイプを作る段階が3回ぐらいあったんです。「こういう感じで動きます」と見せながら、コンテンツパートナーシップ側とも調整して進めていました。

冬鏡: 数段階のプロトタイプ提示のラリーで、仕様まで遡ってブラッシュアップしていく感じですか。

加古: そうですね。バインダー機能は仕様書の変更が結構ありました。途中で大きかったのは、もともとアイテムを自由に配置できなかった仕様を、自由に配置できるようにしたことです。最初はグリッド上にアイテムを並べるだけでした。でも、やっぱり自由に配置できた方が楽しくないか、という話になっていきました。

冬鏡: そのプロトタイプは API まで作り込んだ状態だったんですか。

加古: バインダー機能の開発の流れは少し特殊で、API はまだ薄い状態でした。フロントエンドだけ作って見せる、という形に近かったと思います。保存などもまだちゃんとできていない段階で、まず見通しを立てるためのプロトタイプでした。

資料: グリッド配置から自由配置へ

グリッド配置から自由配置へ変更することで、体験の自由度と実装難度が上がった。

最初のバインダー機能は、カードやアイテムをグリッド上に置く仕様でした。これは実装上は扱いやすい一方で、ユーザーが自分で作っている感覚は弱くなります。

自由配置にすると、実装難度は上がります。

  • アイテムを任意の位置に置ける。
  • 拡大・縮小・回転を自然に扱う必要がある。
  • アイテム同士の重なりや衝突判定が必要になる。
  • タッチ操作の途中状態を丁寧に管理する必要がある。

それでも自由配置にしたのは、バインダーの体験として「自分だけの場所を作る」感覚を強めるためでした。

開発者が仕様書を書く

冬鏡: バインダー機能は、開発が仕様に関わる割合がいつもより多かった感じなんですか。

加古: 仕様のあたりでいうと、バインダー機能は初稿の仕様書を僕が書いています。それをプロダクトチームの方が、人に見せられるものにアップデートしてくれました。仕様書の作成にはエンジニアとして関わりましたが、もともと僕がソーシャルゲームを遊ぶユーザーでもあったため、その視点も活かしています。

冬鏡: ソーシャルゲームだと、バインダーっぽい機能は見かけますよね。

加古: そうですね。カードを飾るような機能は見かけます。仕様書では「なんでこの機能を作るのか」という整理から始めました。どう遊ぶか、という機能面の整理を僕が書いて、売上やプロダクト上の狙いのような専門的なところをプロダクトチーム側が整理してくれた形です。

冬鏡: バインダー機能に開発が最初から関わっていたのは、ソーシャルゲームの知識や、仕様書を最初に書いたという個人的な事情もありましたか。

加古: あるかもしれないですね。バインダーはもともと仕様書があったものの、なかなか企画が進んでいませんでした。森田さんが進めたいと思っているところに、僕も「カードの使い道が増えたら面白いだろうな」と思っていたので、手助けしに行った側面は強いと思います。

「正しい」仕様かどうかは、リリースまで分からない

冬鏡: 開発側から仕様書をゼロから書くことには、どんな難しさがありましたか。

加古: 難しいのは、「正しい」かどうかはリリースするまで分からないことです。つまり、これでちゃんと楽しんでもらえるかどうかです。

たとえば「自由配置にする」という判断も、グリッドより楽しいはずだと思って決めるわけですが、本当にそれが「正しい」かは出してみるまで分かりません。グリッドをやめて自由配置にするのは大きな決断でした。

冬鏡: 仕様のまま進めるか、変えるかの判断が大変だった。

加古: そうですね。もう少し自由度を上げた方が楽しいんじゃないか、という判断がありました。

資料: 仕様判断と実装難度はつながっている

体験を優先した仕様判断が、Canvas上の座標変換や衝突判定の複雑さにつながる。

「自由配置の方が楽しそう」という仕様判断は、そのまま実装の複雑さにつながります。自由配置を選ぶと、Canvas 上で次のような処理を自前で扱う必要があります。

  • 座標変換
  • 拡大縮小
  • 回転
  • 衝突判定
  • 複数ポインターの管理
  • 描画ループのパフォーマンス

バインダーでは、プロダクトとしての楽しさを優先した結果、フロントエンドの作り込みが必要な設計になりました。

Canvasでゲームに近い描画をする

冬鏡: 実装の話に移ると、バインダーは普段あまりやらない実装が多いですよね。たとえば 60 FPS で描画するような、ORICAL ではあまりなかった実装です。

加古: そうですね。結構ゲーム寄りに近い書き方をしていると思います。Canvas に requestAnimationFrame で描画しているので、本当にゲームみたいな感じです。

冬鏡: カードの中には、静止画ではなく動画を表示するものもありますよね。

加古: 動画は video 要素を Canvas にそのまま描画できます。video 要素を autoplay と loop にしておいて、画像を描画するときと同じように描けばできます。ただ、裏で autoplay した状態の video 要素を持っておく必要があるので、その管理はしています。

資料: バインダー機能の主要モジュール

約3000行規模のドメインロジックを、移動・衝突判定・ジェスチャーなどの役割で整理した。

バインダー機能のドメインロジックは約3000行規模で、主なモジュールは次のように整理されました。

  • base.ts: メイン composable、イベントハンドリング
  • collision.ts: 衝突判定
  • move.ts: アイテム移動、スケール、回転
  • gesture.ts: ピンチズーム認識
  • sprites.ts: UI ボタンスプライト管理
  • draw.ts: Canvas 描画ユーティリティ
  • stage.ts: ステージサイズ計算

通常の DOM ベースの UI では、ボタンやフォームの基本的な振る舞いをブラウザに任せられます。Canvas の場合は、指を動かしたときに何が起きるべきかをアプリケーション側で定義する必要があります。

回転、移動、衝突判定を自前で扱う

冬鏡: base.ts がかなり重めで、他にも衝突判定やアイテム移動、回転のモジュールが大きいですよね。そこは苦労しましたか。

加古: Canvas を使っているので、ボタンを押したら何が起こるか、指を動かしたらどう動くか、全部自分で書かないといけません。

たとえば、ある点からある点に指を動かしたとき、どう動くのが自然なのかをずっと考えていました。回転なら、指が動いたときに何度回転するのが正解なのか。中心からの距離を測って、角度を測って、それを数式に起こして実装する必要があります。

冬鏡: 線形代数や三角関数を使ったと言っていましたね。

加古: 回転は三角関数系ですね。Canvas は XY 座標なので、回転系の座標は三角関数で変換します。衝突判定も大変でした。ただ、アイテムは全部長方形なので、凸多角形であることが保証されています。そこは少し楽になる点でした。

冬鏡: 計算量も気にしないといけない。

加古: 重い処理をするとすぐ処理落ちします。なので、ループの回数には気を使っています。forEach や map のような書き方がきれいなこともありますが、それだと使いにくいアルゴリズムもあります。速さを重視して for 文を使うこともあります。

資料: Canvas上の「自然な操作」は数式でできている

自然に見える操作も、内部では移動量・回転角・衝突判定などの計算で成り立っている。

ユーザーから見ると、カードをつまんで動かしたり、回転させたりするだけです。しかし内部では、次のような処理が走ります。

  • 指の移動量から、アイテムの移動量を計算する。
  • 中心点と指の位置から、回転角を計算する。
  • 拡大縮小の中心と倍率を計算する。
  • 回転した長方形同士が重なっているか判定する。
  • 描画ループ内で処理が重くならないようにする。

見た目の自然さは、座標計算とパフォーマンス設計の積み重ねです。

ピンチ操作と複数ポインターの管理

冬鏡: ピンチやドラッグのイベント管理はどうでしたか。昔触ったときは、認識が安定しなくてうまく拡大できなかった記憶があります。

加古: ちゃんと実装しようと思うと大変です。たとえば、人間は2本の指を完全に同時には置けません。1本目を置いてから、少し遅れて2本目が置かれます。そういうときの処理を考えないといけません。

あとは、2本指で拡大中にもう1本指を置いたらどうするのか。急に3本目の指を置いたらどうするのか。そういうケースをちゃんと考えないと、バグを引き起こしやすいです。

資料: タッチ操作で起きるエッジケース

ピンチやドラッグでは、指の増減や状態遷移を考慮しないと意図しない挙動につながる。

  • 2本指が完全同時には置かれない。
  • ピンチ中に片方の指だけが離れる。
  • ピンチ中に3本目の指が置かれる。
  • ドラッグからピンチへ、ピンチからドラッグへ状態が変わる。
  • 操作中のアイテムと他アイテムの衝突状態が変わる。

こうした途中状態を曖昧に扱うと、アイテムが急に飛ぶ、拡大率が壊れる、意図しない回転が起きる、といった問題につながります。

WeakMapで画像管理を切り離す

冬鏡: WeakMap でキャッシュしている部分もありました。これはどういう意図ですか。

加古: あるアイテムのオブジェクトに対する画像データは、画像を描画する処理のところでしか使いません。なので、オブジェクトに WeakMap で画像を結びつけておくと管理が楽です。

オブジェクトがなくなったら、WeakMap ならメモリも自動で解放されます。画像描画の部分を切り離せるのが便利でした。

冬鏡: 逆に、思ったより楽だった部分はありましたか。

加古: Canvas の描画関数は結構充実していました。回転も関数でできますし、video を直接描くのも、画像を引数に与えるところに video を入れればそのまま動きます。そのあたりは Canvas API が充実していると思います。

実装していて面白かったところ

冬鏡: 実装していて楽しかったところや、気に入っているところはありますか。

加古: メインの部分が一番面白かったんじゃないですかね。バインダー機能にはランキングなどのサブ機能もありますが、ランキングは API から返ってくるものをそのまま表示するだけに近いです。

一方で、カードを配置していくメインの部分は、自分が作ったものがそのままプレイヤー体験につながります。大変ではありましたが、やればできあがるものなので、そこは面白かったです。

冬鏡: バックエンドとは違う、フロントエンドの作り込みの余地がある場所ですね。

加古: そうですね。メイン部分の実装はほぼ僕がやっていました。

横展開しやすいUIにする

加古: バインダー機能には、まだ話していないアピールポイントがあります。

ORICAL の課題として、パートナーへの横展開が大変という問題があります。

パートナーごとにやりたいことが出てくるので、個別対応が増えやすいんです。そこでバインダー機能では、できる限りそれを少なくするために、UI をすごくシンプルにしました。

冬鏡: パートナーごとのカスタム余地を減らして、横展開しやすくした。

加古: そうですね。個性はアイテムで出してもらうことにして、UI 自体はシンプルに抑えました。実際、そのおかげでバインダー機能はすぐ他のパートナーに広がったと思います。

ロビー画面も、背景1枚とボタンだけです。ボタンもパートナーごとに作っているわけではなく、パートナーのプライマリーカラーごとに決めています。新規パートナーが増えても、ほぼ自動的に決められるようにしています。

資料: 横展開を前提にしたUI設計

パートナーごとに作り込まず、共通UIと色差分で横展開できる設計にした。

バインダー機能では、パートナーごとに画面を作り込むのではなく、共通 UI と色差分で展開できるようにしました。

  • パートナー固有の個性はカードやアイテムで表現する。
  • バインダー本体の UI はできるだけシンプルにする。
  • ロビー画面は背景とボタンを中心に構成する。
  • ボタン色はパートナー単位ではなく、プライマリーカラー単位で決める。

これは、デザインを簡素にしたというだけではありません。複数パートナーへ低コストで展開するためのプロダクト設計です。

目的を理解して作る

冬鏡: 仕様を書くところから運用まで、いつもより早い段階から関わってみてどうでしたか。

加古: やっぱり、物を作るにあたって、それを作る目的が分かっているとだいぶやりやすいです。目的が分かっていないと、言われたものをただ作るだけになってしまいます。ちゃんと理解して、その上で提案できたらさらにいいと思います。

グリッド配置の仕様を最初に作ったときも、なぜグリッドにしようと思ったかというと、自由度が高すぎたらユーザーが大変じゃないか、という考えがありました。ORICAL は野球プロダクトでもあるので、スマホにすごく慣れている人ばかりではないかもしれない。そういうユーザー像を考えることも大事です。

冬鏡: 作りながらユーザー像を理解していく方が、ただ資料などを見て勉強するより分かりやすい。

加古: そうですね。実感を伴って理解できるというか、心で理解できる感じがあります。

これから入るフロントエンド開発者に期待すること

冬鏡: これからフロントエンドにジョインされる方に期待したいことはありますか。

加古: ありきたりな答えかもしれませんが、言われたことをやるだけではなく、自分で考えられる人だといいと思います。

「この仕様で本当にいいんですか」とか、「これを実装するとコード管理が大変になりそうです」といったところに気づけることです。ORICAL は管理が大変なプロダクトなので、自分でメリット・デメリットを考えられるとありがたいです。

冬鏡: やることの種類も多様ですし、いろいろな事情がある中で、自立して動いてくれる人がありがたいということですね。

加古: そうですね。

おわりに

バインダー機能は、Canvas を使ったゲーム的な実装が目立つ機能です。衝突判定、回転、ピンチ操作、描画キャッシュなど、技術的にも面白い要素が多くあります。

ただし今回のインタビューで印象的だったのは、技術的な工夫が単独で存在しているわけではないことでした。

自由配置にするからこそ、衝突判定やジェスチャー実装が必要になる。横展開を重視するからこそ、UI をシンプルにする。目的を理解しているからこそ、実装者が仕様にも踏み込んで判断できる。

HEARTBEATS では、こうした形でプロダクトの目的と実装を行き来しながら開発しています。フロントエンドの作り込みを通じて、ユーザー体験と事業上の展開しやすさの両方を考える。その一例として、バインダー機能開発の裏側を紹介しました。

HEARTBEATSでは開発系職種も積極採用中です。この記事に書かれているような課題に興味を持った方、あるいはプロダクトに興味を持った方がいたら、ぜひ気軽にご応募ください!