🎰

恵方巻きガッチャしよう

に公開

暦はすでに二月となり、立春を迎え、節分の月となった。
古人の曰く、光陰矢の如し...なんちゃって!

さてさて、みんな様は恵方巻きを食べていますか?
恵方巻きの具材を悩んでいるのでしょうか。定番具材7種だと言われ、それ以外もたくさん挙がられる恵方巻きですが、何を入れるのがなかなか悩ましいですよね。
そんなあなたに、ピッタリなアプリを作りました!

背景

今年1月に入社した新米社員ですが、OJT研修の一環として、Vueを用いたアプリ開発に挑戦する機会をいただきました。
旨は面白さを重視しつつ、今度の全社懇親会でみんなが一緒に「遊べるもの」であること、また時節が節分であることを踏まえ、恵方巻きガッチャアプリを作成することにしました。
全社懇親会、盛り上がるといいですね!

機能

このアプリは以下4つの機能に構成されています。

  • 恵方巻き具材抽選機能
  • 詳細設定機能
    • 具材名、個数編集
    • 一回のガッチャで排出具材個数設定(3 or 4、重複可)
    • 演出動画オンオフ設定
  • ガッチャ履歴機能
    • 履歴閲覧
    • 履歴クリア
  • 恵方巻き動画演出機能

完成品

  • トップ画面

  • 詳細設定画面

  • 履歴画面

コンポーネント構成

コンポーネントは下記の通りに構成されいます。

App
 |- AnimationModal
 |- EditModal
 |- HistoryModal
 | |- ConfirmModal
 |- SlotReel

それぞれの担当画面
App: トップ画面
AnimationModal: 動画演出モーダル画面
EditModal: 詳細設定モーダル画面
HistoryModal: 履歴画面モーダル画面
ConfirmModal: 履歴クリアダブルチェック用モーダル画面
SlotReel: ガッチャスロット

ガッチャ機能、特にガッチャスロットに関してもう少し説明しましょう。

ガッチャスロットに関して

今回のガッチャスロットの実装において、コアとなる SlotReel コンポーネントの仕組みについて解説します。
イメージとしては、一つの SlotReel コンポーネントが一つのスロットをコントロールし、ガッチャ開始後スロットの回転アニメーションを演出します。
 * 以下のgif画像通りに、 SlotReel コンポーネントが4つ並ぶイメージです

ガッチャスロットのロジックは以下となります。

1. 具材をランダムに抽選

各具材の個数がそれぞれなので、均等性を考慮した故に、アルゴリズムは Roulette Wheel Selection を使用しました。
https://en.wikipedia.org/wiki/Fitness_proportionate_selection

Appでの抽選結果をパラメータとしてSlotReelに送り、SlotReelpropsによりデータを受け取ります。

2. スロットの初期設定

スロットの回転を表現するために、実際にDOM要素を無限にループさせるのではなく、事前に結果を含む長いリスト📜を作成し、それを上から下へ一気にスライドさせる という手法を採用しました。
そのリスト📜は、以下の部分に構成されます。

  • 初期表示部: 初期表示用のアイテム('?'など)
  • 回転演出部: スロットが回るビジュアル演出ために、具材名をランダムに連結
  • 最終結果部: props で渡された抽選結果をリスト📜の一番最後に配置

これにより、スロットどれだけ回転しても、リスト📜の一番下にスライドすれば必ず指定された 抽選結果 で止まることになります。

3. アニメーション制御

回転アニメーションは CSS の transform: translateYtransition を JS から操作することで実現しています。

  • スロットリセット: まず CSS の transition を無効化し、translateY を 0 に設定するより、リストの最上部に戻します。これにより、前回の回転終了位置から瞬時にスタート位置へ戻りますが、ユーザーの目には「回転が始まった」ように見えます。
  • DOM更新完了まで待機: Vue の nextTicksetTimeout を挟むことで、リセット処理が確実にブラウザに描画されるのを待ちます。これを行わないと、ブラウザの最適化によりリセット動作がスキップされ、アニメーションが正常に動作しない場合があります。
  • target への移動(アニメーション開始): CSS の transition を有効化します。同時に、リストの最下部(target の位置)までの距離を計算し、translateY の値を設定します。
const isAnimating = ref(false);
const currentTranslateY = ref(0);
const itemHeightPx = 100; // css関連:スロットの高さ(px)
...
const reelStyle = computed(() => ({
  transform: `translateY(${currentTranslateY.value}px)`,
  transition: isAnimating.value 
    ? `transform ${props.animationDurationMs}ms cubic-bezier(0.45, 0.05, 0.55, 0.95)` 
    : 'none',
    ...
}));
...
const startRoll = async () => {
  // 1. ロットリセット(アニメーション無し)
  resetRollToTop();

  // 2. DOM更新完了まで待機
  await nextTick();
  await new Promise(resolve => setTimeout(resolve, 30));

  // 3. アニメーション開始 → 最終位置へ
  isAnimating.value = true;
  const finalIndex = displayItems.value.length - 1;
  currentTranslateY.value = -(finalIndex * itemHeightPx);
  ...
}

スロットへのこだわり

恵方巻き演出動画の裏話

  • 現状: 恵方巻きの動画演出は、あらかじめ Google Gemini で生成したものをアプリ内のローカルに保存し、ランダムにピックアップして再生する方式を採用しています。 これにより、ネットワーク状況に左右されず、ガッチャ結果が出た瞬間に「即座に」高品質な演出を表示することを可能できます。

  • やりたかったこと: 実はアプリの設計段階では、より面白い案も挙げられました。それは、抽選結果をプロンプトとしてAIに渡し、結果に応じて動画を生成することでした。
    例えば、穴子、焼きたまご、海老が出たら、この三つの具材で恵方巻きをその場で巻くことが考えられるし、焼きたまごが三つ出たら少し面白い巻き巻き動画も盛り上がれるのでしょう。

  • 見送った理由: APIコスト及び生成時間によるUXの低下を懸念しました。
    2026年2月現在、精度の低い Gemini Veo Fast でも、一秒あたり $0.15 かかるし、スロットを回すたびに課金が発生するモデルは、個人開発アプリの運用コストとして現実的ではありませんでした。
    さらに、数秒の動画を生成するのに数十秒〜数分の待ち時間が発生します。
    スロットマシンの醍醐味は「テンポの良さ」と「結果の即時性」にありながら、結果を見るために「動画生成中...」と数分待たされては、ゲームとして成り立てませんよね。

現在のAI技術では、このガッチャゲームに活用するのはまだまだ難しいです。

終わりに

春をもっと楽しもう!!!

wwwave's Techblog

Discussion